Browse Source

implement draft/read-marker capability

tags/v2.10.0-rc1
Shivaram Lingamneni 2 years ago
parent
commit
32f7868bfd
11 changed files with 190 additions and 79 deletions
  1. 6
    0
      gencapdefs.py
  2. 15
    3
      irc/accounts.go
  3. 6
    1
      irc/caps/defs.go
  4. 11
    2
      irc/channel.go
  5. 8
    59
      irc/client.go
  6. 5
    1
      irc/commands.go
  7. 57
    0
      irc/getters.go
  8. 44
    0
      irc/handlers.go
  9. 7
    0
      irc/help.go
  10. 20
    13
      irc/server.go
  11. 11
    0
      irc/strings.go

+ 6
- 0
gencapdefs.py View File

@@ -183,6 +183,12 @@ CAPDEFS = [
183 183
         url="https://github.com/ircv3/ircv3-specifications/pull/466",
184 184
         standard="draft IRCv3",
185 185
     ),
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
+    ),
186 192
 ]
187 193
 
188 194
 def validate_defs():

+ 15
- 3
irc/accounts.go View File

@@ -41,6 +41,7 @@ const (
41 41
 	keyCertToAccount           = "account.creds.certfp %s"
42 42
 	keyAccountChannels         = "account.channels %s" // channels registered to the account
43 43
 	keyAccountLastSeen         = "account.lastseen %s"
44
+	keyAccountReadMarkers      = "account.readmarkers %s"
44 45
 	keyAccountModes            = "account.modes %s"     // user modes for the always-on client as a string
45 46
 	keyAccountRealname         = "account.realname %s"  // client realname stored as string
46 47
 	keyAccountSuspended        = "account.suspended %s" // client realname stored as string
@@ -647,9 +648,18 @@ func (am *AccountManager) loadModes(account string) (uModes modes.Modes) {
647 648
 
648 649
 func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.Time) {
649 650
 	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) {
650 660
 	var val string
651
-	if len(lastSeen) != 0 {
652
-		text, _ := json.Marshal(lastSeen)
661
+	if len(timeMap) != 0 {
662
+		text, _ := json.Marshal(timeMap)
653 663
 		val = string(text)
654 664
 	}
655 665
 	err := am.server.store.Update(func(tx *buntdb.Tx) error {
@@ -661,7 +671,7 @@ func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.
661 671
 		return nil
662 672
 	})
663 673
 	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())
665 675
 	}
666 676
 }
667 677
 
@@ -1739,6 +1749,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
1739 1749
 	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
1740 1750
 	joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
1741 1751
 	lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
1752
+	readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
1742 1753
 	unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
1743 1754
 	modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
1744 1755
 	realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
@@ -1801,6 +1812,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
1801 1812
 		tx.Delete(channelsKey)
1802 1813
 		tx.Delete(joinedChannelsKey)
1803 1814
 		tx.Delete(lastSeenKey)
1815
+		tx.Delete(readMarkersKey)
1804 1816
 		tx.Delete(modesKey)
1805 1817
 		tx.Delete(realnameKey)
1806 1818
 		tx.Delete(suspendedKey)

+ 6
- 1
irc/caps/defs.go View File

@@ -7,7 +7,7 @@ package caps
7 7
 
8 8
 const (
9 9
 	// number of recognized capabilities:
10
-	numCapabs = 28
10
+	numCapabs = 29
11 11
 	// length of the uint64 array that represents the bitset:
12 12
 	bitsetLen = 1
13 13
 )
@@ -65,6 +65,10 @@ const (
65 65
 	// https://github.com/ircv3/ircv3-specifications/pull/398
66 66
 	Multiline Capability = iota
67 67
 
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
+
68 72
 	// Relaymsg is the proposed IRCv3 capability named "draft/relaymsg":
69 73
 	// https://github.com/ircv3/ircv3-specifications/pull/417
70 74
 	Relaymsg Capability = iota
@@ -142,6 +146,7 @@ var (
142 146
 		"draft/extended-monitor",
143 147
 		"draft/languages",
144 148
 		"draft/multiline",
149
+		"draft/read-marker",
145 150
 		"draft/relaymsg",
146 151
 		"echo-message",
147 152
 		"ergo.chat/nope",

+ 11
- 2
irc/channel.go View File

@@ -881,6 +881,10 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
881 881
 		rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "JOIN", chname)
882 882
 	}
883 883
 
884
+	if rb.session.capabilities.Has(caps.ReadMarker) {
885
+		rb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
886
+	}
887
+
884 888
 	if rb.session.client == client {
885 889
 		// don't send topic and names for a SAJOIN of a different client
886 890
 		channel.SendTopic(client, rb, false)
@@ -964,10 +968,15 @@ func (channel *Channel) playJoinForSession(session *Session) {
964 968
 	client := session.client
965 969
 	sessionRb := NewResponseBuffer(session)
966 970
 	details := client.Details()
971
+	chname := channel.Name()
967 972
 	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)
969 974
 	} 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))
971 980
 	}
972 981
 	channel.SendTopic(client, sessionRb, false)
973 982
 	channel.Names(client, sessionRb)

+ 8
- 59
irc/client.go View File

@@ -40,9 +40,9 @@ const (
40 40
 	IRCv3TimestampFormat = utils.IRCv3TimestampFormat
41 41
 	// limit the number of device IDs a client can use, as a DoS mitigation
42 42
 	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
46 46
 )
47 47
 
48 48
 const (
@@ -83,7 +83,7 @@ type Client struct {
83 83
 	languages          []string
84 84
 	lastActive         time.Time            // last time they sent a command that wasn't PONG or similar
85 85
 	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
87 87
 	loginThrottle      connection_limits.GenericThrottle
88 88
 	nextSessionID      int64 // Incremented when a new session is established
89 89
 	nick               string
@@ -101,6 +101,7 @@ type Client struct {
101 101
 	requireSASL        bool
102 102
 	registered         bool
103 103
 	registerCmdSent    bool // already sent the draft/register command, can't send it again
104
+	dirtyTimestamps    bool // lastSeen or readMarkers is dirty
104 105
 	registrationTimer  *time.Timer
105 106
 	server             *Server
106 107
 	skeleton           string
@@ -745,41 +746,23 @@ func (client *Client) playReattachMessages(session *Session) {
745 746
 // Touch indicates that we received a line from the client (so the connection is healthy
746 747
 // at this time, modulo network latency and fakelag).
747 748
 func (client *Client) Touch(session *Session) {
748
-	var markDirty bool
749 749
 	now := time.Now().UTC()
750 750
 	client.stateMutex.Lock()
751 751
 	if client.registered {
752 752
 		client.updateIdleTimer(session, now)
753 753
 		if client.alwaysOn {
754 754
 			client.setLastSeen(now, session.deviceID)
755
-			if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
756
-				markDirty = true
757
-				client.lastSeenLastWrite = now
758
-			}
755
+			client.dirtyTimestamps = true
759 756
 		}
760 757
 	}
761 758
 	client.stateMutex.Unlock()
762
-	if markDirty {
763
-		client.markDirty(IncludeLastSeen)
764
-	}
765 759
 }
766 760
 
767 761
 func (client *Client) setLastSeen(now time.Time, deviceID string) {
768 762
 	if client.lastSeen == nil {
769 763
 		client.lastSeen = make(map[string]time.Time)
770 764
 	}
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)
783 766
 }
784 767
 
785 768
 func (client *Client) updateIdleTimer(session *Session, now time.Time) {
@@ -1191,7 +1174,6 @@ func (client *Client) Quit(message string, session *Session) {
1191 1174
 func (client *Client) destroy(session *Session) {
1192 1175
 	config := client.server.Config()
1193 1176
 	var sessionsToDestroy []*Session
1194
-	var saveLastSeen bool
1195 1177
 	var quitMessage string
1196 1178
 
1197 1179
 	client.stateMutex.Lock()
@@ -1223,20 +1205,6 @@ func (client *Client) destroy(session *Session) {
1223 1205
 		}
1224 1206
 	}
1225 1207
 
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
-
1240 1208
 	// should we destroy the whole client this time?
1241 1209
 	shouldDestroy := !client.destroyed && remainingSessions == 0 && !alwaysOn
1242 1210
 	// 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) {
1246 1214
 		// if it's our job to destroy it, don't let anyone else try
1247 1215
 		client.destroyed = true
1248 1216
 	}
1249
-	if saveLastSeen {
1250
-		client.dirtyBits |= IncludeLastSeen
1251
-	}
1252 1217
 
1253 1218
 	becameAutoAway := false
1254 1219
 	var awayMessage string
@@ -1266,14 +1231,6 @@ func (client *Client) destroy(session *Session) {
1266 1231
 
1267 1232
 	client.stateMutex.Unlock()
1268 1233
 
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
-
1277 1234
 	// destroy all applicable sessions:
1278 1235
 	for _, session := range sessionsToDestroy {
1279 1236
 		if session.client != client {
@@ -1784,18 +1741,13 @@ func (client *Client) handleRegisterTimeout() {
1784 1741
 func (client *Client) copyLastSeen() (result map[string]time.Time) {
1785 1742
 	client.stateMutex.RLock()
1786 1743
 	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)
1792 1745
 }
1793 1746
 
1794 1747
 // these are bit flags indicating what part of the client status is "dirty"
1795 1748
 // and needs to be read from memory and written to the db
1796 1749
 const (
1797 1750
 	IncludeChannels uint = 1 << iota
1798
-	IncludeLastSeen
1799 1751
 	IncludeUserModes
1800 1752
 	IncludeRealname
1801 1753
 )
@@ -1853,9 +1805,6 @@ func (client *Client) performWrite(additionalDirtyBits uint) {
1853 1805
 		}
1854 1806
 		client.server.accounts.saveChannels(account, channelToModes)
1855 1807
 	}
1856
-	if (dirtyBits & IncludeLastSeen) != 0 {
1857
-		client.server.accounts.saveLastSeen(account, client.copyLastSeen())
1858
-	}
1859 1808
 	if (dirtyBits & IncludeUserModes) != 0 {
1860 1809
 		uModes := make(modes.Modes, 0, len(modes.SupportedUserModes))
1861 1810
 		for _, m := range modes.SupportedUserModes {

+ 5
- 1
irc/commands.go View File

@@ -53,7 +53,7 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
53 53
 	}
54 54
 
55 55
 	if client.registered {
56
-		client.Touch(session)
56
+		client.Touch(session) // even if `exiting`, we bump the lastSeen timestamp
57 57
 	}
58 58
 
59 59
 	return exiting
@@ -178,6 +178,10 @@ func init() {
178 178
 			handler:   lusersHandler,
179 179
 			minParams: 0,
180 180
 		},
181
+		"MARKREAD": {
182
+			handler:   markReadHandler,
183
+			minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS
184
+		},
181 185
 		"MODE": {
182 186
 			handler:   modeHandler,
183 187
 			minParams: 1,

+ 57
- 0
irc/getters.go View File

@@ -493,6 +493,63 @@ func (client *Client) checkAlwaysOnExpirationNoMutex(config *Config, ignoreRegis
493 493
 	return true
494 494
 }
495 495
 
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
+
496 553
 func (channel *Channel) Name() string {
497 554
 	channel.stateMutex.RLock()
498 555
 	defer channel.stateMutex.RUnlock()

+ 44
- 0
irc/handlers.go View File

@@ -2700,6 +2700,50 @@ func verifyHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
2700 2700
 	return
2701 2701
 }
2702 2702
 
2703
+// MARKREAD <target> [timestamp]
2704
+func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) {
2705
+	if len(msg.Params) == 0 {
2706
+		rb.Add(nil, server.name, "FAIL", "MARKREAD", "NEED_MORE_PARAMS", client.t("Missing parameters"))
2707
+		return
2708
+	}
2709
+
2710
+	target := msg.Params[0]
2711
+	cftarget, err := CasefoldTarget(target)
2712
+	if err != nil {
2713
+		rb.Add(nil, server.name, "FAIL", "MARKREAD", "INVALID_PARAMS", utils.SafeErrorParam(target), client.t("Invalid target"))
2714
+		return
2715
+	}
2716
+	unfoldedTarget := server.UnfoldName(cftarget)
2717
+
2718
+	// "MARKREAD client get command": MARKREAD <target>
2719
+	if len(msg.Params) == 1 {
2720
+		rb.Add(nil, client.server.name, "MARKREAD", unfoldedTarget, client.GetReadMarker(cftarget))
2721
+		return
2722
+	}
2723
+
2724
+	// "MARKREAD client set command": MARKREAD <target> <timestamp>
2725
+	readTimestamp := msg.Params[1]
2726
+	readTime, err := time.Parse(IRCv3TimestampFormat, readTimestamp)
2727
+	if err != nil {
2728
+		rb.Add(nil, server.name, "FAIL", "MARKREAD", "INVALID_PARAMS", utils.SafeErrorParam(readTimestamp), client.t("Invalid timestamp"))
2729
+		return
2730
+	}
2731
+	result := client.SetReadMarker(cftarget, readTime)
2732
+	readTimestamp = result.Format(IRCv3TimestampFormat)
2733
+	// inform the originating session whether it was a success or a no-op:
2734
+	rb.Add(nil, server.name, "MARKREAD", unfoldedTarget, readTimestamp)
2735
+	if result.Equal(readTime) {
2736
+		// successful update (i.e. it moved the stored timestamp forward):
2737
+		// inform other sessions
2738
+		for _, session := range client.Sessions() {
2739
+			if session != rb.session {
2740
+				session.Send(nil, server.name, "MARKREAD", unfoldedTarget, readTimestamp)
2741
+			}
2742
+		}
2743
+	}
2744
+	return
2745
+}
2746
+
2703 2747
 // REHASH
2704 2748
 func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
2705 2749
 	nick := client.Nick()

+ 7
- 0
irc/help.go View File

@@ -320,6 +320,13 @@ channels). <elistcond>s modify how the channels are selected.`,
320 320
 Shows statistics about the size of the network. If <mask> is given, only
321 321
 returns stats for servers matching the given mask.  If <server> is given, the
322 322
 command is processed by that server.`,
323
+	},
324
+	"markread": {
325
+		text: `MARKREAD <target> [timestamp]
326
+
327
+MARKREAD updates an IRCv3 read message marker. It is not intended for use by
328
+end users. For more details, see the latest draft of the read-marker
329
+specification.`,
323 330
 	},
324 331
 	"mode": {
325 332
 		text: `MODE <target> [<modestring> [<mode arguments>...]]

+ 20
- 13
irc/server.go View File

@@ -36,7 +36,7 @@ import (
36 36
 )
37 37
 
38 38
 const (
39
-	alwaysOnExpirationPollPeriod = time.Hour
39
+	alwaysOnMaintenanceInterval = 30 * time.Minute
40 40
 )
41 41
 
42 42
 var (
@@ -119,7 +119,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
119 119
 	signal.Notify(server.exitSignals, utils.ServerExitSignals...)
120 120
 	signal.Notify(server.rehashSignal, syscall.SIGHUP)
121 121
 
122
-	time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
122
+	time.AfterFunc(alwaysOnMaintenanceInterval, server.periodicAlwaysOnMaintenance)
123 123
 
124 124
 	return server, nil
125 125
 }
@@ -132,11 +132,11 @@ func (server *Server) Shutdown() {
132 132
 	//TODO(dan): Make sure we disallow new nicks
133 133
 	for _, client := range server.clients.AllClients() {
134 134
 		client.Notice("Server is shutting down")
135
-		if client.AlwaysOn() {
136
-			client.Store(IncludeLastSeen)
137
-		}
138 135
 	}
139 136
 
137
+	// flush data associated with always-on clients:
138
+	server.performAlwaysOnMaintenance(false, true)
139
+
140 140
 	if err := server.store.Close(); err != nil {
141 141
 		server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
142 142
 	}
@@ -244,25 +244,32 @@ func (server *Server) checkTorLimits() (banned bool, message string) {
244 244
 	}
245 245
 }
246 246
 
247
-func (server *Server) handleAlwaysOnExpirations() {
247
+func (server *Server) periodicAlwaysOnMaintenance() {
248 248
 	defer func() {
249 249
 		// reschedule whether or not there was a panic
250
-		time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
250
+		time.AfterFunc(alwaysOnMaintenanceInterval, server.periodicAlwaysOnMaintenance)
251 251
 	}()
252 252
 
253 253
 	defer server.HandlePanic()
254 254
 
255
+	server.logger.Info("accounts", "Performing periodic always-on client checks")
256
+	server.performAlwaysOnMaintenance(true, true)
257
+}
258
+
259
+func (server *Server) performAlwaysOnMaintenance(checkExpiration, flushTimestamps bool) {
255 260
 	config := server.Config()
256
-	deadline := time.Duration(config.Accounts.Multiclient.AlwaysOnExpiration)
257
-	if deadline == 0 {
258
-		return
259
-	}
260
-	server.logger.Info("accounts", "Checking always-on clients for expiration")
261 261
 	for _, client := range server.clients.AllClients() {
262
-		if client.IsExpiredAlwaysOn(config) {
262
+		if checkExpiration && client.IsExpiredAlwaysOn(config) {
263 263
 			// TODO save the channels list, use it for autojoin if/when they return?
264 264
 			server.logger.Info("accounts", "Expiring always-on client", client.AccountName())
265 265
 			client.destroy(nil)
266
+			continue
267
+		}
268
+
269
+		if flushTimestamps && client.shouldFlushTimestamps() {
270
+			account := client.Account()
271
+			server.accounts.saveLastSeen(account, client.copyLastSeen())
272
+			server.accounts.saveReadMarkers(account, client.copyReadMarkers())
266 273
 		}
267 274
 	}
268 275
 }

+ 11
- 0
irc/strings.go View File

@@ -165,6 +165,17 @@ func CasefoldName(name string) (string, error) {
165 165
 	return lowered, err
166 166
 }
167 167
 
168
+// CasefoldTarget returns a casefolded version of an IRC target, i.e.
169
+// it determines whether the target is a channel name or nickname and
170
+// applies the appropriate casefolding rules.
171
+func CasefoldTarget(name string) (string, error) {
172
+	if strings.HasPrefix(name, "#") {
173
+		return CasefoldChannel(name)
174
+	} else {
175
+		return CasefoldName(name)
176
+	}
177
+}
178
+
168 179
 // returns true if the given name is a valid ident, using a mix of Insp and
169 180
 // Chary's ident restrictions.
170 181
 func isIdent(name string) bool {

Loading…
Cancel
Save