Skip to content

Commit 1f08c97

Browse files
authored
Merge pull request #1926 from slingamn/readmarker.6
implement draft/read-marker capability
2 parents 2c488f5 + 1adda8d commit 1f08c97

File tree

12 files changed

+191
-80
lines changed

12 files changed

+191
-80
lines changed

gencapdefs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@
183183
url="https://github.com/ircv3/ircv3-specifications/pull/466",
184184
standard="draft IRCv3",
185185
),
186+
CapDef(
187+
identifier="ReadMarker",
188+
name="draft/read-marker",
189+
url="https://github.com/ircv3/ircv3-specifications/pull/489",
190+
standard="draft IRCv3",
191+
),
186192
]
187193

188194
def validate_defs():

irc/accounts.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const (
4141
keyCertToAccount = "account.creds.certfp %s"
4242
keyAccountChannels = "account.channels %s" // channels registered to the account
4343
keyAccountLastSeen = "account.lastseen %s"
44+
keyAccountReadMarkers = "account.readmarkers %s"
4445
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
4546
keyAccountRealname = "account.realname %s" // client realname stored as string
4647
keyAccountSuspended = "account.suspended %s" // client realname stored as string
@@ -647,9 +648,18 @@ func (am *AccountManager) loadModes(account string) (uModes modes.Modes) {
647648

648649
func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.Time) {
649650
key := fmt.Sprintf(keyAccountLastSeen, account)
651+
am.saveTimeMap(account, key, lastSeen)
652+
}
653+
654+
func (am *AccountManager) saveReadMarkers(account string, readMarkers map[string]time.Time) {
655+
key := fmt.Sprintf(keyAccountReadMarkers, account)
656+
am.saveTimeMap(account, key, readMarkers)
657+
}
658+
659+
func (am *AccountManager) saveTimeMap(account, key string, timeMap map[string]time.Time) {
650660
var val string
651-
if len(lastSeen) != 0 {
652-
text, _ := json.Marshal(lastSeen)
661+
if len(timeMap) != 0 {
662+
text, _ := json.Marshal(timeMap)
653663
val = string(text)
654664
}
655665
err := am.server.store.Update(func(tx *buntdb.Tx) error {
@@ -661,7 +671,7 @@ func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.
661671
return nil
662672
})
663673
if err != nil {
664-
am.server.logger.Error("internal", "error persisting lastSeen", account, err.Error())
674+
am.server.logger.Error("internal", "error persisting timeMap", key, err.Error())
665675
}
666676
}
667677

@@ -1739,6 +1749,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
17391749
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
17401750
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
17411751
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
1752+
readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
17421753
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
17431754
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
17441755
realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
@@ -1801,6 +1812,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
18011812
tx.Delete(channelsKey)
18021813
tx.Delete(joinedChannelsKey)
18031814
tx.Delete(lastSeenKey)
1815+
tx.Delete(readMarkersKey)
18041816
tx.Delete(modesKey)
18051817
tx.Delete(realnameKey)
18061818
tx.Delete(suspendedKey)

irc/caps/defs.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package caps
77

88
const (
99
// number of recognized capabilities:
10-
numCapabs = 28
10+
numCapabs = 29
1111
// length of the uint64 array that represents the bitset:
1212
bitsetLen = 1
1313
)
@@ -65,6 +65,10 @@ const (
6565
// https://github.com/ircv3/ircv3-specifications/pull/398
6666
Multiline Capability = iota
6767

68+
// ReadMarker is the draft IRCv3 capability named "draft/read-marker":
69+
// https://github.com/ircv3/ircv3-specifications/pull/489
70+
ReadMarker Capability = iota
71+
6872
// Relaymsg is the proposed IRCv3 capability named "draft/relaymsg":
6973
// https://github.com/ircv3/ircv3-specifications/pull/417
7074
Relaymsg Capability = iota
@@ -142,6 +146,7 @@ var (
142146
"draft/extended-monitor",
143147
"draft/languages",
144148
"draft/multiline",
149+
"draft/read-marker",
145150
"draft/relaymsg",
146151
"echo-message",
147152
"ergo.chat/nope",

irc/channel.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,10 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
881881
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "JOIN", chname)
882882
}
883883

884+
if rb.session.capabilities.Has(caps.ReadMarker) {
885+
rb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
886+
}
887+
884888
if rb.session.client == client {
885889
// don't send topic and names for a SAJOIN of a different client
886890
channel.SendTopic(client, rb, false)
@@ -964,10 +968,15 @@ func (channel *Channel) playJoinForSession(session *Session) {
964968
client := session.client
965969
sessionRb := NewResponseBuffer(session)
966970
details := client.Details()
971+
chname := channel.Name()
967972
if session.capabilities.Has(caps.ExtendedJoin) {
968-
sessionRb.Add(nil, details.nickMask, "JOIN", channel.Name(), details.accountName, details.realname)
973+
sessionRb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
969974
} else {
970-
sessionRb.Add(nil, details.nickMask, "JOIN", channel.Name())
975+
sessionRb.Add(nil, details.nickMask, "JOIN", chname)
976+
}
977+
if session.capabilities.Has(caps.ReadMarker) {
978+
chcfname := channel.NameCasefolded()
979+
sessionRb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
971980
}
972981
channel.SendTopic(client, sessionRb, false)
973982
channel.Names(client, sessionRb)

irc/client.go

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ const (
4040
IRCv3TimestampFormat = utils.IRCv3TimestampFormat
4141
// limit the number of device IDs a client can use, as a DoS mitigation
4242
maxDeviceIDsPerClient = 64
43-
// controls how often often we write an autoreplay-missed client's
44-
// deviceid->lastseentime mapping to the database
45-
lastSeenWriteInterval = time.Hour
43+
// maximum total read markers that can be stored
44+
// (writeback of read markers is controlled by lastSeen logic)
45+
maxReadMarkers = 256
4646
)
4747

4848
const (
@@ -83,7 +83,7 @@ type Client struct {
8383
languages []string
8484
lastActive time.Time // last time they sent a command that wasn't PONG or similar
8585
lastSeen map[string]time.Time // maps device ID (including "") to time of last received command
86-
lastSeenLastWrite time.Time // last time `lastSeen` was written to the datastore
86+
readMarkers map[string]time.Time // maps casefolded target to time of last read marker
8787
loginThrottle connection_limits.GenericThrottle
8888
nextSessionID int64 // Incremented when a new session is established
8989
nick string
@@ -101,6 +101,7 @@ type Client struct {
101101
requireSASL bool
102102
registered bool
103103
registerCmdSent bool // already sent the draft/register command, can't send it again
104+
dirtyTimestamps bool // lastSeen or readMarkers is dirty
104105
registrationTimer *time.Timer
105106
server *Server
106107
skeleton string
@@ -745,41 +746,23 @@ func (client *Client) playReattachMessages(session *Session) {
745746
// Touch indicates that we received a line from the client (so the connection is healthy
746747
// at this time, modulo network latency and fakelag).
747748
func (client *Client) Touch(session *Session) {
748-
var markDirty bool
749749
now := time.Now().UTC()
750750
client.stateMutex.Lock()
751751
if client.registered {
752752
client.updateIdleTimer(session, now)
753753
if client.alwaysOn {
754754
client.setLastSeen(now, session.deviceID)
755-
if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
756-
markDirty = true
757-
client.lastSeenLastWrite = now
758-
}
755+
client.dirtyTimestamps = true
759756
}
760757
}
761758
client.stateMutex.Unlock()
762-
if markDirty {
763-
client.markDirty(IncludeLastSeen)
764-
}
765759
}
766760

767761
func (client *Client) setLastSeen(now time.Time, deviceID string) {
768762
if client.lastSeen == nil {
769763
client.lastSeen = make(map[string]time.Time)
770764
}
771-
client.lastSeen[deviceID] = now
772-
// evict the least-recently-used entry if necessary
773-
if maxDeviceIDsPerClient < len(client.lastSeen) {
774-
var minLastSeen time.Time
775-
var minClientId string
776-
for deviceID, lastSeen := range client.lastSeen {
777-
if minLastSeen.IsZero() || lastSeen.Before(minLastSeen) {
778-
minClientId, minLastSeen = deviceID, lastSeen
779-
}
780-
}
781-
delete(client.lastSeen, minClientId)
782-
}
765+
updateLRUMap(client.lastSeen, deviceID, now, maxDeviceIDsPerClient)
783766
}
784767

785768
func (client *Client) updateIdleTimer(session *Session, now time.Time) {
@@ -1191,7 +1174,6 @@ func (client *Client) Quit(message string, session *Session) {
11911174
func (client *Client) destroy(session *Session) {
11921175
config := client.server.Config()
11931176
var sessionsToDestroy []*Session
1194-
var saveLastSeen bool
11951177
var quitMessage string
11961178

11971179
client.stateMutex.Lock()
@@ -1223,20 +1205,6 @@ func (client *Client) destroy(session *Session) {
12231205
}
12241206
}
12251207

1226-
// save last seen if applicable:
1227-
if alwaysOn {
1228-
if client.accountSettings.AutoreplayMissed {
1229-
saveLastSeen = true
1230-
} else {
1231-
for _, session := range sessionsToDestroy {
1232-
if session.deviceID != "" {
1233-
saveLastSeen = true
1234-
break
1235-
}
1236-
}
1237-
}
1238-
}
1239-
12401208
// should we destroy the whole client this time?
12411209
shouldDestroy := !client.destroyed && remainingSessions == 0 && !alwaysOn
12421210
// 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) {
12461214
// if it's our job to destroy it, don't let anyone else try
12471215
client.destroyed = true
12481216
}
1249-
if saveLastSeen {
1250-
client.dirtyBits |= IncludeLastSeen
1251-
}
12521217

12531218
becameAutoAway := false
12541219
var awayMessage string
@@ -1266,14 +1231,6 @@ func (client *Client) destroy(session *Session) {
12661231

12671232
client.stateMutex.Unlock()
12681233

1269-
// XXX there is no particular reason to persist this state here rather than
1270-
// any other place: it would be correct to persist it after every `Touch`. However,
1271-
// I'm not comfortable introducing that many database writes, and I don't want to
1272-
// design a throttle.
1273-
if saveLastSeen {
1274-
client.wakeWriter()
1275-
}
1276-
12771234
// destroy all applicable sessions:
12781235
for _, session := range sessionsToDestroy {
12791236
if session.client != client {
@@ -1784,18 +1741,13 @@ func (client *Client) handleRegisterTimeout() {
17841741
func (client *Client) copyLastSeen() (result map[string]time.Time) {
17851742
client.stateMutex.RLock()
17861743
defer client.stateMutex.RUnlock()
1787-
result = make(map[string]time.Time, len(client.lastSeen))
1788-
for id, lastSeen := range client.lastSeen {
1789-
result[id] = lastSeen
1790-
}
1791-
return
1744+
return utils.CopyMap(client.lastSeen)
17921745
}
17931746

17941747
// these are bit flags indicating what part of the client status is "dirty"
17951748
// and needs to be read from memory and written to the db
17961749
const (
17971750
IncludeChannels uint = 1 << iota
1798-
IncludeLastSeen
17991751
IncludeUserModes
18001752
IncludeRealname
18011753
)
@@ -1853,9 +1805,6 @@ func (client *Client) performWrite(additionalDirtyBits uint) {
18531805
}
18541806
client.server.accounts.saveChannels(account, channelToModes)
18551807
}
1856-
if (dirtyBits & IncludeLastSeen) != 0 {
1857-
client.server.accounts.saveLastSeen(account, client.copyLastSeen())
1858-
}
18591808
if (dirtyBits & IncludeUserModes) != 0 {
18601809
uModes := make(modes.Modes, 0, len(modes.SupportedUserModes))
18611810
for _, m := range modes.SupportedUserModes {

irc/commands.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
5353
}
5454

5555
if client.registered {
56-
client.Touch(session)
56+
client.Touch(session) // even if `exiting`, we bump the lastSeen timestamp
5757
}
5858

5959
return exiting
@@ -178,6 +178,10 @@ func init() {
178178
handler: lusersHandler,
179179
minParams: 0,
180180
},
181+
"MARKREAD": {
182+
handler: markReadHandler,
183+
minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS
184+
},
181185
"MODE": {
182186
handler: modeHandler,
183187
minParams: 1,

irc/getters.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,63 @@ func (client *Client) checkAlwaysOnExpirationNoMutex(config *Config, ignoreRegis
493493
return true
494494
}
495495

496+
func (client *Client) GetReadMarker(cfname string) (result string) {
497+
client.stateMutex.RLock()
498+
t, ok := client.readMarkers[cfname]
499+
client.stateMutex.RUnlock()
500+
if ok {
501+
return t.Format(IRCv3TimestampFormat)
502+
}
503+
return "*"
504+
}
505+
506+
func (client *Client) copyReadMarkers() (result map[string]time.Time) {
507+
client.stateMutex.RLock()
508+
defer client.stateMutex.RUnlock()
509+
return utils.CopyMap(client.readMarkers)
510+
}
511+
512+
func (client *Client) SetReadMarker(cfname string, now time.Time) (result time.Time) {
513+
client.stateMutex.Lock()
514+
defer client.stateMutex.Unlock()
515+
516+
if client.readMarkers == nil {
517+
client.readMarkers = make(map[string]time.Time)
518+
}
519+
result = updateLRUMap(client.readMarkers, cfname, now, maxReadMarkers)
520+
client.dirtyTimestamps = true
521+
return
522+
}
523+
524+
func updateLRUMap(lru map[string]time.Time, key string, val time.Time, maxItems int) (result time.Time) {
525+
if currentVal := lru[key]; currentVal.After(val) {
526+
return currentVal
527+
}
528+
529+
lru[key] = val
530+
// evict the least-recently-used entry if necessary
531+
if maxItems < len(lru) {
532+
var minKey string
533+
var minVal time.Time
534+
for key, val := range lru {
535+
if minVal.IsZero() || val.Before(minVal) {
536+
minKey, minVal = key, val
537+
}
538+
}
539+
delete(lru, minKey)
540+
}
541+
return val
542+
}
543+
544+
func (client *Client) shouldFlushTimestamps() (result bool) {
545+
client.stateMutex.Lock()
546+
defer client.stateMutex.Unlock()
547+
548+
result = client.dirtyTimestamps && client.registered && client.alwaysOn
549+
client.dirtyTimestamps = false
550+
return
551+
}
552+
496553
func (channel *Channel) Name() string {
497554
channel.stateMutex.RLock()
498555
defer channel.stateMutex.RUnlock()

0 commit comments

Comments
 (0)