mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 05:19:25 +01:00
implement draft/read-marker capability
This commit is contained in:
parent
6bd94391ef
commit
32f7868bfd
@ -183,6 +183,12 @@ CAPDEFS = [
|
|||||||
url="https://github.com/ircv3/ircv3-specifications/pull/466",
|
url="https://github.com/ircv3/ircv3-specifications/pull/466",
|
||||||
standard="draft IRCv3",
|
standard="draft IRCv3",
|
||||||
),
|
),
|
||||||
|
CapDef(
|
||||||
|
identifier="ReadMarker",
|
||||||
|
name="draft/read-marker",
|
||||||
|
url="https://github.com/ircv3/ircv3-specifications/pull/489",
|
||||||
|
standard="draft IRCv3",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_defs():
|
def validate_defs():
|
||||||
|
@ -41,6 +41,7 @@ const (
|
|||||||
keyCertToAccount = "account.creds.certfp %s"
|
keyCertToAccount = "account.creds.certfp %s"
|
||||||
keyAccountChannels = "account.channels %s" // channels registered to the account
|
keyAccountChannels = "account.channels %s" // channels registered to the account
|
||||||
keyAccountLastSeen = "account.lastseen %s"
|
keyAccountLastSeen = "account.lastseen %s"
|
||||||
|
keyAccountReadMarkers = "account.readmarkers %s"
|
||||||
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
||||||
keyAccountRealname = "account.realname %s" // client realname stored as string
|
keyAccountRealname = "account.realname %s" // client realname stored as string
|
||||||
keyAccountSuspended = "account.suspended %s" // client realname stored as string
|
keyAccountSuspended = "account.suspended %s" // client realname stored as string
|
||||||
@ -647,9 +648,18 @@ func (am *AccountManager) loadModes(account string) (uModes modes.Modes) {
|
|||||||
|
|
||||||
func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.Time) {
|
func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.Time) {
|
||||||
key := fmt.Sprintf(keyAccountLastSeen, account)
|
key := fmt.Sprintf(keyAccountLastSeen, account)
|
||||||
|
am.saveTimeMap(account, key, lastSeen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AccountManager) saveReadMarkers(account string, readMarkers map[string]time.Time) {
|
||||||
|
key := fmt.Sprintf(keyAccountReadMarkers, account)
|
||||||
|
am.saveTimeMap(account, key, readMarkers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AccountManager) saveTimeMap(account, key string, timeMap map[string]time.Time) {
|
||||||
var val string
|
var val string
|
||||||
if len(lastSeen) != 0 {
|
if len(timeMap) != 0 {
|
||||||
text, _ := json.Marshal(lastSeen)
|
text, _ := json.Marshal(timeMap)
|
||||||
val = string(text)
|
val = string(text)
|
||||||
}
|
}
|
||||||
err := am.server.store.Update(func(tx *buntdb.Tx) error {
|
err := am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
@ -661,7 +671,7 @@ func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", "error persisting lastSeen", account, err.Error())
|
am.server.logger.Error("internal", "error persisting timeMap", key, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1739,6 +1749,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
||||||
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
|
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
|
||||||
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
||||||
|
readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
|
||||||
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
|
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
|
||||||
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
|
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
|
||||||
realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
|
realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
|
||||||
@ -1801,6 +1812,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
tx.Delete(channelsKey)
|
tx.Delete(channelsKey)
|
||||||
tx.Delete(joinedChannelsKey)
|
tx.Delete(joinedChannelsKey)
|
||||||
tx.Delete(lastSeenKey)
|
tx.Delete(lastSeenKey)
|
||||||
|
tx.Delete(readMarkersKey)
|
||||||
tx.Delete(modesKey)
|
tx.Delete(modesKey)
|
||||||
tx.Delete(realnameKey)
|
tx.Delete(realnameKey)
|
||||||
tx.Delete(suspendedKey)
|
tx.Delete(suspendedKey)
|
||||||
|
@ -7,7 +7,7 @@ package caps
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 28
|
numCapabs = 29
|
||||||
// length of the uint64 array that represents the bitset:
|
// length of the uint64 array that represents the bitset:
|
||||||
bitsetLen = 1
|
bitsetLen = 1
|
||||||
)
|
)
|
||||||
@ -65,6 +65,10 @@ const (
|
|||||||
// https://github.com/ircv3/ircv3-specifications/pull/398
|
// https://github.com/ircv3/ircv3-specifications/pull/398
|
||||||
Multiline Capability = iota
|
Multiline Capability = iota
|
||||||
|
|
||||||
|
// ReadMarker is the draft IRCv3 capability named "draft/read-marker":
|
||||||
|
// https://github.com/ircv3/ircv3-specifications/pull/489
|
||||||
|
ReadMarker Capability = iota
|
||||||
|
|
||||||
// Relaymsg is the proposed IRCv3 capability named "draft/relaymsg":
|
// Relaymsg is the proposed IRCv3 capability named "draft/relaymsg":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/417
|
// https://github.com/ircv3/ircv3-specifications/pull/417
|
||||||
Relaymsg Capability = iota
|
Relaymsg Capability = iota
|
||||||
@ -142,6 +146,7 @@ var (
|
|||||||
"draft/extended-monitor",
|
"draft/extended-monitor",
|
||||||
"draft/languages",
|
"draft/languages",
|
||||||
"draft/multiline",
|
"draft/multiline",
|
||||||
|
"draft/read-marker",
|
||||||
"draft/relaymsg",
|
"draft/relaymsg",
|
||||||
"echo-message",
|
"echo-message",
|
||||||
"ergo.chat/nope",
|
"ergo.chat/nope",
|
||||||
|
@ -881,6 +881,10 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "JOIN", chname)
|
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "JOIN", chname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rb.session.capabilities.Has(caps.ReadMarker) {
|
||||||
|
rb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
|
||||||
|
}
|
||||||
|
|
||||||
if rb.session.client == client {
|
if 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)
|
||||||
@ -964,10 +968,15 @@ func (channel *Channel) playJoinForSession(session *Session) {
|
|||||||
client := session.client
|
client := session.client
|
||||||
sessionRb := NewResponseBuffer(session)
|
sessionRb := NewResponseBuffer(session)
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
|
chname := channel.Name()
|
||||||
if session.capabilities.Has(caps.ExtendedJoin) {
|
if session.capabilities.Has(caps.ExtendedJoin) {
|
||||||
sessionRb.Add(nil, details.nickMask, "JOIN", channel.Name(), details.accountName, details.realname)
|
sessionRb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
||||||
} else {
|
} else {
|
||||||
sessionRb.Add(nil, details.nickMask, "JOIN", channel.Name())
|
sessionRb.Add(nil, details.nickMask, "JOIN", chname)
|
||||||
|
}
|
||||||
|
if session.capabilities.Has(caps.ReadMarker) {
|
||||||
|
chcfname := channel.NameCasefolded()
|
||||||
|
sessionRb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
|
||||||
}
|
}
|
||||||
channel.SendTopic(client, sessionRb, false)
|
channel.SendTopic(client, sessionRb, false)
|
||||||
channel.Names(client, sessionRb)
|
channel.Names(client, sessionRb)
|
||||||
|
@ -40,9 +40,9 @@ const (
|
|||||||
IRCv3TimestampFormat = utils.IRCv3TimestampFormat
|
IRCv3TimestampFormat = utils.IRCv3TimestampFormat
|
||||||
// limit the number of device IDs a client can use, as a DoS mitigation
|
// limit the number of device IDs a client can use, as a DoS mitigation
|
||||||
maxDeviceIDsPerClient = 64
|
maxDeviceIDsPerClient = 64
|
||||||
// controls how often often we write an autoreplay-missed client's
|
// maximum total read markers that can be stored
|
||||||
// deviceid->lastseentime mapping to the database
|
// (writeback of read markers is controlled by lastSeen logic)
|
||||||
lastSeenWriteInterval = time.Hour
|
maxReadMarkers = 256
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -83,7 +83,7 @@ type Client struct {
|
|||||||
languages []string
|
languages []string
|
||||||
lastActive time.Time // last time they sent a command that wasn't PONG or similar
|
lastActive time.Time // last time they sent a command that wasn't PONG or similar
|
||||||
lastSeen map[string]time.Time // maps device ID (including "") to time of last received command
|
lastSeen map[string]time.Time // maps device ID (including "") to time of last received command
|
||||||
lastSeenLastWrite time.Time // last time `lastSeen` was written to the datastore
|
readMarkers map[string]time.Time // maps casefolded target to time of last read marker
|
||||||
loginThrottle connection_limits.GenericThrottle
|
loginThrottle connection_limits.GenericThrottle
|
||||||
nextSessionID int64 // Incremented when a new session is established
|
nextSessionID int64 // Incremented when a new session is established
|
||||||
nick string
|
nick string
|
||||||
@ -101,6 +101,7 @@ type Client struct {
|
|||||||
requireSASL bool
|
requireSASL bool
|
||||||
registered bool
|
registered bool
|
||||||
registerCmdSent bool // already sent the draft/register command, can't send it again
|
registerCmdSent bool // already sent the draft/register command, can't send it again
|
||||||
|
dirtyTimestamps bool // lastSeen or readMarkers is dirty
|
||||||
registrationTimer *time.Timer
|
registrationTimer *time.Timer
|
||||||
server *Server
|
server *Server
|
||||||
skeleton string
|
skeleton string
|
||||||
@ -745,41 +746,23 @@ func (client *Client) playReattachMessages(session *Session) {
|
|||||||
// Touch indicates that we received a line from the client (so the connection is healthy
|
// Touch indicates that we received a line from the client (so the connection is healthy
|
||||||
// at this time, modulo network latency and fakelag).
|
// at this time, modulo network latency and fakelag).
|
||||||
func (client *Client) Touch(session *Session) {
|
func (client *Client) Touch(session *Session) {
|
||||||
var markDirty bool
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
if client.registered {
|
if client.registered {
|
||||||
client.updateIdleTimer(session, now)
|
client.updateIdleTimer(session, now)
|
||||||
if client.alwaysOn {
|
if client.alwaysOn {
|
||||||
client.setLastSeen(now, session.deviceID)
|
client.setLastSeen(now, session.deviceID)
|
||||||
if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
|
client.dirtyTimestamps = true
|
||||||
markDirty = true
|
|
||||||
client.lastSeenLastWrite = now
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
if markDirty {
|
|
||||||
client.markDirty(IncludeLastSeen)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) setLastSeen(now time.Time, deviceID string) {
|
func (client *Client) setLastSeen(now time.Time, deviceID string) {
|
||||||
if client.lastSeen == nil {
|
if client.lastSeen == nil {
|
||||||
client.lastSeen = make(map[string]time.Time)
|
client.lastSeen = make(map[string]time.Time)
|
||||||
}
|
}
|
||||||
client.lastSeen[deviceID] = now
|
updateLRUMap(client.lastSeen, deviceID, now, maxDeviceIDsPerClient)
|
||||||
// evict the least-recently-used entry if necessary
|
|
||||||
if maxDeviceIDsPerClient < len(client.lastSeen) {
|
|
||||||
var minLastSeen time.Time
|
|
||||||
var minClientId string
|
|
||||||
for deviceID, lastSeen := range client.lastSeen {
|
|
||||||
if minLastSeen.IsZero() || lastSeen.Before(minLastSeen) {
|
|
||||||
minClientId, minLastSeen = deviceID, lastSeen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete(client.lastSeen, minClientId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) updateIdleTimer(session *Session, now time.Time) {
|
func (client *Client) updateIdleTimer(session *Session, now time.Time) {
|
||||||
@ -1191,7 +1174,6 @@ func (client *Client) Quit(message string, session *Session) {
|
|||||||
func (client *Client) destroy(session *Session) {
|
func (client *Client) destroy(session *Session) {
|
||||||
config := client.server.Config()
|
config := client.server.Config()
|
||||||
var sessionsToDestroy []*Session
|
var sessionsToDestroy []*Session
|
||||||
var saveLastSeen bool
|
|
||||||
var quitMessage string
|
var quitMessage string
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
@ -1223,20 +1205,6 @@ func (client *Client) destroy(session *Session) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// save last seen if applicable:
|
|
||||||
if alwaysOn {
|
|
||||||
if client.accountSettings.AutoreplayMissed {
|
|
||||||
saveLastSeen = true
|
|
||||||
} else {
|
|
||||||
for _, session := range sessionsToDestroy {
|
|
||||||
if session.deviceID != "" {
|
|
||||||
saveLastSeen = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// should we destroy the whole client this time?
|
// should we destroy the whole client this time?
|
||||||
shouldDestroy := !client.destroyed && remainingSessions == 0 && !alwaysOn
|
shouldDestroy := !client.destroyed && remainingSessions == 0 && !alwaysOn
|
||||||
// decrement stats on a true destroy, or for the removal of the last connected session
|
// decrement stats on a true destroy, or for the removal of the last connected session
|
||||||
@ -1246,9 +1214,6 @@ func (client *Client) destroy(session *Session) {
|
|||||||
// 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 saveLastSeen {
|
|
||||||
client.dirtyBits |= IncludeLastSeen
|
|
||||||
}
|
|
||||||
|
|
||||||
becameAutoAway := false
|
becameAutoAway := false
|
||||||
var awayMessage string
|
var awayMessage string
|
||||||
@ -1266,14 +1231,6 @@ func (client *Client) destroy(session *Session) {
|
|||||||
|
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
|
|
||||||
// XXX there is no particular reason to persist this state here rather than
|
|
||||||
// any other place: it would be correct to persist it after every `Touch`. However,
|
|
||||||
// I'm not comfortable introducing that many database writes, and I don't want to
|
|
||||||
// design a throttle.
|
|
||||||
if saveLastSeen {
|
|
||||||
client.wakeWriter()
|
|
||||||
}
|
|
||||||
|
|
||||||
// destroy all applicable sessions:
|
// destroy all applicable sessions:
|
||||||
for _, session := range sessionsToDestroy {
|
for _, session := range sessionsToDestroy {
|
||||||
if session.client != client {
|
if session.client != client {
|
||||||
@ -1784,18 +1741,13 @@ func (client *Client) handleRegisterTimeout() {
|
|||||||
func (client *Client) copyLastSeen() (result map[string]time.Time) {
|
func (client *Client) copyLastSeen() (result map[string]time.Time) {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
result = make(map[string]time.Time, len(client.lastSeen))
|
return utils.CopyMap(client.lastSeen)
|
||||||
for id, lastSeen := range client.lastSeen {
|
|
||||||
result[id] = lastSeen
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// these are bit flags indicating what part of the client status is "dirty"
|
// 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
|
// and needs to be read from memory and written to the db
|
||||||
const (
|
const (
|
||||||
IncludeChannels uint = 1 << iota
|
IncludeChannels uint = 1 << iota
|
||||||
IncludeLastSeen
|
|
||||||
IncludeUserModes
|
IncludeUserModes
|
||||||
IncludeRealname
|
IncludeRealname
|
||||||
)
|
)
|
||||||
@ -1853,9 +1805,6 @@ func (client *Client) performWrite(additionalDirtyBits uint) {
|
|||||||
}
|
}
|
||||||
client.server.accounts.saveChannels(account, channelToModes)
|
client.server.accounts.saveChannels(account, channelToModes)
|
||||||
}
|
}
|
||||||
if (dirtyBits & IncludeLastSeen) != 0 {
|
|
||||||
client.server.accounts.saveLastSeen(account, client.copyLastSeen())
|
|
||||||
}
|
|
||||||
if (dirtyBits & IncludeUserModes) != 0 {
|
if (dirtyBits & IncludeUserModes) != 0 {
|
||||||
uModes := make(modes.Modes, 0, len(modes.SupportedUserModes))
|
uModes := make(modes.Modes, 0, len(modes.SupportedUserModes))
|
||||||
for _, m := range modes.SupportedUserModes {
|
for _, m := range modes.SupportedUserModes {
|
||||||
|
@ -53,7 +53,7 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
|
|||||||
}
|
}
|
||||||
|
|
||||||
if client.registered {
|
if client.registered {
|
||||||
client.Touch(session)
|
client.Touch(session) // even if `exiting`, we bump the lastSeen timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
return exiting
|
return exiting
|
||||||
@ -178,6 +178,10 @@ func init() {
|
|||||||
handler: lusersHandler,
|
handler: lusersHandler,
|
||||||
minParams: 0,
|
minParams: 0,
|
||||||
},
|
},
|
||||||
|
"MARKREAD": {
|
||||||
|
handler: markReadHandler,
|
||||||
|
minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS
|
||||||
|
},
|
||||||
"MODE": {
|
"MODE": {
|
||||||
handler: modeHandler,
|
handler: modeHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
|
@ -493,6 +493,63 @@ func (client *Client) checkAlwaysOnExpirationNoMutex(config *Config, ignoreRegis
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetReadMarker(cfname string) (result string) {
|
||||||
|
client.stateMutex.RLock()
|
||||||
|
t, ok := client.readMarkers[cfname]
|
||||||
|
client.stateMutex.RUnlock()
|
||||||
|
if ok {
|
||||||
|
return t.Format(IRCv3TimestampFormat)
|
||||||
|
}
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) copyReadMarkers() (result map[string]time.Time) {
|
||||||
|
client.stateMutex.RLock()
|
||||||
|
defer client.stateMutex.RUnlock()
|
||||||
|
return utils.CopyMap(client.readMarkers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) SetReadMarker(cfname string, now time.Time) (result time.Time) {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
defer client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if client.readMarkers == nil {
|
||||||
|
client.readMarkers = make(map[string]time.Time)
|
||||||
|
}
|
||||||
|
result = updateLRUMap(client.readMarkers, cfname, now, maxReadMarkers)
|
||||||
|
client.dirtyTimestamps = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLRUMap(lru map[string]time.Time, key string, val time.Time, maxItems int) (result time.Time) {
|
||||||
|
if currentVal := lru[key]; currentVal.After(val) {
|
||||||
|
return currentVal
|
||||||
|
}
|
||||||
|
|
||||||
|
lru[key] = val
|
||||||
|
// evict the least-recently-used entry if necessary
|
||||||
|
if maxItems < len(lru) {
|
||||||
|
var minKey string
|
||||||
|
var minVal time.Time
|
||||||
|
for key, val := range lru {
|
||||||
|
if minVal.IsZero() || val.Before(minVal) {
|
||||||
|
minKey, minVal = key, val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(lru, minKey)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) shouldFlushTimestamps() (result bool) {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
defer client.stateMutex.Unlock()
|
||||||
|
|
||||||
|
result = client.dirtyTimestamps && client.registered && client.alwaysOn
|
||||||
|
client.dirtyTimestamps = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (channel *Channel) Name() string {
|
func (channel *Channel) Name() string {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
|
@ -2700,6 +2700,50 @@ func verifyHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARKREAD <target> [timestamp]
|
||||||
|
func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) {
|
||||||
|
if len(msg.Params) == 0 {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "MARKREAD", "NEED_MORE_PARAMS", client.t("Missing parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
target := msg.Params[0]
|
||||||
|
cftarget, err := CasefoldTarget(target)
|
||||||
|
if err != nil {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "MARKREAD", "INVALID_PARAMS", utils.SafeErrorParam(target), client.t("Invalid target"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unfoldedTarget := server.UnfoldName(cftarget)
|
||||||
|
|
||||||
|
// "MARKREAD client get command": MARKREAD <target>
|
||||||
|
if len(msg.Params) == 1 {
|
||||||
|
rb.Add(nil, client.server.name, "MARKREAD", unfoldedTarget, client.GetReadMarker(cftarget))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// "MARKREAD client set command": MARKREAD <target> <timestamp>
|
||||||
|
readTimestamp := msg.Params[1]
|
||||||
|
readTime, err := time.Parse(IRCv3TimestampFormat, readTimestamp)
|
||||||
|
if err != nil {
|
||||||
|
rb.Add(nil, server.name, "FAIL", "MARKREAD", "INVALID_PARAMS", utils.SafeErrorParam(readTimestamp), client.t("Invalid timestamp"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result := client.SetReadMarker(cftarget, readTime)
|
||||||
|
readTimestamp = result.Format(IRCv3TimestampFormat)
|
||||||
|
// inform the originating session whether it was a success or a no-op:
|
||||||
|
rb.Add(nil, server.name, "MARKREAD", unfoldedTarget, readTimestamp)
|
||||||
|
if result.Equal(readTime) {
|
||||||
|
// successful update (i.e. it moved the stored timestamp forward):
|
||||||
|
// inform other sessions
|
||||||
|
for _, session := range client.Sessions() {
|
||||||
|
if session != rb.session {
|
||||||
|
session.Send(nil, server.name, "MARKREAD", unfoldedTarget, readTimestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// REHASH
|
// REHASH
|
||||||
func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||||
nick := client.Nick()
|
nick := client.Nick()
|
||||||
|
@ -320,6 +320,13 @@ channels). <elistcond>s modify how the channels are selected.`,
|
|||||||
Shows statistics about the size of the network. If <mask> is given, only
|
Shows statistics about the size of the network. If <mask> is given, only
|
||||||
returns stats for servers matching the given mask. If <server> is given, the
|
returns stats for servers matching the given mask. If <server> is given, the
|
||||||
command is processed by that server.`,
|
command is processed by that server.`,
|
||||||
|
},
|
||||||
|
"markread": {
|
||||||
|
text: `MARKREAD <target> [timestamp]
|
||||||
|
|
||||||
|
MARKREAD updates an IRCv3 read message marker. It is not intended for use by
|
||||||
|
end users. For more details, see the latest draft of the read-marker
|
||||||
|
specification.`,
|
||||||
},
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
text: `MODE <target> [<modestring> [<mode arguments>...]]
|
text: `MODE <target> [<modestring> [<mode arguments>...]]
|
||||||
|
@ -36,7 +36,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
alwaysOnExpirationPollPeriod = time.Hour
|
alwaysOnMaintenanceInterval = 30 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -119,7 +119,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
|||||||
signal.Notify(server.exitSignals, utils.ServerExitSignals...)
|
signal.Notify(server.exitSignals, utils.ServerExitSignals...)
|
||||||
signal.Notify(server.rehashSignal, syscall.SIGHUP)
|
signal.Notify(server.rehashSignal, syscall.SIGHUP)
|
||||||
|
|
||||||
time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
|
time.AfterFunc(alwaysOnMaintenanceInterval, server.periodicAlwaysOnMaintenance)
|
||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
@ -132,11 +132,11 @@ func (server *Server) Shutdown() {
|
|||||||
//TODO(dan): Make sure we disallow new nicks
|
//TODO(dan): Make sure we disallow new nicks
|
||||||
for _, client := range server.clients.AllClients() {
|
for _, client := range server.clients.AllClients() {
|
||||||
client.Notice("Server is shutting down")
|
client.Notice("Server is shutting down")
|
||||||
if client.AlwaysOn() {
|
|
||||||
client.Store(IncludeLastSeen)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// flush data associated with always-on clients:
|
||||||
|
server.performAlwaysOnMaintenance(false, true)
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
@ -244,25 +244,32 @@ func (server *Server) checkTorLimits() (banned bool, message string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) handleAlwaysOnExpirations() {
|
func (server *Server) periodicAlwaysOnMaintenance() {
|
||||||
defer func() {
|
defer func() {
|
||||||
// reschedule whether or not there was a panic
|
// reschedule whether or not there was a panic
|
||||||
time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
|
time.AfterFunc(alwaysOnMaintenanceInterval, server.periodicAlwaysOnMaintenance)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
defer server.HandlePanic()
|
defer server.HandlePanic()
|
||||||
|
|
||||||
|
server.logger.Info("accounts", "Performing periodic always-on client checks")
|
||||||
|
server.performAlwaysOnMaintenance(true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) performAlwaysOnMaintenance(checkExpiration, flushTimestamps bool) {
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
deadline := time.Duration(config.Accounts.Multiclient.AlwaysOnExpiration)
|
|
||||||
if deadline == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
server.logger.Info("accounts", "Checking always-on clients for expiration")
|
|
||||||
for _, client := range server.clients.AllClients() {
|
for _, client := range server.clients.AllClients() {
|
||||||
if client.IsExpiredAlwaysOn(config) {
|
if checkExpiration && client.IsExpiredAlwaysOn(config) {
|
||||||
// TODO save the channels list, use it for autojoin if/when they return?
|
// TODO save the channels list, use it for autojoin if/when they return?
|
||||||
server.logger.Info("accounts", "Expiring always-on client", client.AccountName())
|
server.logger.Info("accounts", "Expiring always-on client", client.AccountName())
|
||||||
client.destroy(nil)
|
client.destroy(nil)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if flushTimestamps && client.shouldFlushTimestamps() {
|
||||||
|
account := client.Account()
|
||||||
|
server.accounts.saveLastSeen(account, client.copyLastSeen())
|
||||||
|
server.accounts.saveReadMarkers(account, client.copyReadMarkers())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,17 @@ func CasefoldName(name string) (string, error) {
|
|||||||
return lowered, err
|
return lowered, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CasefoldTarget returns a casefolded version of an IRC target, i.e.
|
||||||
|
// it determines whether the target is a channel name or nickname and
|
||||||
|
// applies the appropriate casefolding rules.
|
||||||
|
func CasefoldTarget(name string) (string, error) {
|
||||||
|
if strings.HasPrefix(name, "#") {
|
||||||
|
return CasefoldChannel(name)
|
||||||
|
} else {
|
||||||
|
return CasefoldName(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// returns true if the given name is a valid ident, using a mix of Insp and
|
// returns true if the given name is a valid ident, using a mix of Insp and
|
||||||
// Chary's ident restrictions.
|
// Chary's ident restrictions.
|
||||||
func isIdent(name string) bool {
|
func isIdent(name string) bool {
|
||||||
|
Loading…
Reference in New Issue
Block a user