Преглед на файлове

implement draft/read-marker capability

tags/v2.10.0-rc1
Shivaram Lingamneni преди 2 години
родител
ревизия
32f7868bfd
променени са 11 файла, в които са добавени 190 реда и са изтрити 79 реда
  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 Целия файл

183
         url="https://github.com/ircv3/ircv3-specifications/pull/466",
183
         url="https://github.com/ircv3/ircv3-specifications/pull/466",
184
         standard="draft IRCv3",
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
 def validate_defs():
194
 def validate_defs():

+ 15
- 3
irc/accounts.go Целия файл

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

+ 6
- 1
irc/caps/defs.go Целия файл

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

+ 11
- 2
irc/channel.go Целия файл

881
 		rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, isBot, nil, "JOIN", chname)
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
 	if rb.session.client == client {
888
 	if rb.session.client == client {
885
 		// don't send topic and names for a SAJOIN of a different client
889
 		// don't send topic and names for a SAJOIN of a different client
886
 		channel.SendTopic(client, rb, false)
890
 		channel.SendTopic(client, rb, false)
964
 	client := session.client
968
 	client := session.client
965
 	sessionRb := NewResponseBuffer(session)
969
 	sessionRb := NewResponseBuffer(session)
966
 	details := client.Details()
970
 	details := client.Details()
971
+	chname := channel.Name()
967
 	if session.capabilities.Has(caps.ExtendedJoin) {
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
 	} else {
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
 	channel.SendTopic(client, sessionRb, false)
981
 	channel.SendTopic(client, sessionRb, false)
973
 	channel.Names(client, sessionRb)
982
 	channel.Names(client, sessionRb)

+ 8
- 59
irc/client.go Целия файл

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

+ 5
- 1
irc/commands.go Целия файл

53
 	}
53
 	}
54
 
54
 
55
 	if client.registered {
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
 	return exiting
59
 	return exiting
178
 			handler:   lusersHandler,
178
 			handler:   lusersHandler,
179
 			minParams: 0,
179
 			minParams: 0,
180
 		},
180
 		},
181
+		"MARKREAD": {
182
+			handler:   markReadHandler,
183
+			minParams: 0, // send FAIL instead of ERR_NEEDMOREPARAMS
184
+		},
181
 		"MODE": {
185
 		"MODE": {
182
 			handler:   modeHandler,
186
 			handler:   modeHandler,
183
 			minParams: 1,
187
 			minParams: 1,

+ 57
- 0
irc/getters.go Целия файл

493
 	return true
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
 func (channel *Channel) Name() string {
553
 func (channel *Channel) Name() string {
497
 	channel.stateMutex.RLock()
554
 	channel.stateMutex.RLock()
498
 	defer channel.stateMutex.RUnlock()
555
 	defer channel.stateMutex.RUnlock()

+ 44
- 0
irc/handlers.go Целия файл

2700
 	return
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
 // REHASH
2747
 // REHASH
2704
 func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
2748
 func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
2705
 	nick := client.Nick()
2749
 	nick := client.Nick()

+ 7
- 0
irc/help.go Целия файл

320
 Shows statistics about the size of the network. If <mask> is given, only
320
 Shows statistics about the size of the network. If <mask> is given, only
321
 returns stats for servers matching the given mask.  If <server> is given, the
321
 returns stats for servers matching the given mask.  If <server> is given, the
322
 command is processed by that server.`,
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
 	"mode": {
331
 	"mode": {
325
 		text: `MODE <target> [<modestring> [<mode arguments>...]]
332
 		text: `MODE <target> [<modestring> [<mode arguments>...]]

+ 20
- 13
irc/server.go Целия файл

36
 )
36
 )
37
 
37
 
38
 const (
38
 const (
39
-	alwaysOnExpirationPollPeriod = time.Hour
39
+	alwaysOnMaintenanceInterval = 30 * time.Minute
40
 )
40
 )
41
 
41
 
42
 var (
42
 var (
119
 	signal.Notify(server.exitSignals, utils.ServerExitSignals...)
119
 	signal.Notify(server.exitSignals, utils.ServerExitSignals...)
120
 	signal.Notify(server.rehashSignal, syscall.SIGHUP)
120
 	signal.Notify(server.rehashSignal, syscall.SIGHUP)
121
 
121
 
122
-	time.AfterFunc(alwaysOnExpirationPollPeriod, server.handleAlwaysOnExpirations)
122
+	time.AfterFunc(alwaysOnMaintenanceInterval, server.periodicAlwaysOnMaintenance)
123
 
123
 
124
 	return server, nil
124
 	return server, nil
125
 }
125
 }
132
 	//TODO(dan): Make sure we disallow new nicks
132
 	//TODO(dan): Make sure we disallow new nicks
133
 	for _, client := range server.clients.AllClients() {
133
 	for _, client := range server.clients.AllClients() {
134
 		client.Notice("Server is shutting down")
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
 	if err := server.store.Close(); err != nil {
140
 	if err := server.store.Close(); err != nil {
141
 		server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
141
 		server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
142
 	}
142
 	}
244
 	}
244
 	}
245
 }
245
 }
246
 
246
 
247
-func (server *Server) handleAlwaysOnExpirations() {
247
+func (server *Server) periodicAlwaysOnMaintenance() {
248
 	defer func() {
248
 	defer func() {
249
 		// reschedule whether or not there was a panic
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
 	defer server.HandlePanic()
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
 	config := server.Config()
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
 	for _, client := range server.clients.AllClients() {
261
 	for _, client := range server.clients.AllClients() {
262
-		if client.IsExpiredAlwaysOn(config) {
262
+		if checkExpiration && client.IsExpiredAlwaysOn(config) {
263
 			// TODO save the channels list, use it for autojoin if/when they return?
263
 			// TODO save the channels list, use it for autojoin if/when they return?
264
 			server.logger.Info("accounts", "Expiring always-on client", client.AccountName())
264
 			server.logger.Info("accounts", "Expiring always-on client", client.AccountName())
265
 			client.destroy(nil)
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 Целия файл

165
 	return lowered, err
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
 // returns true if the given name is a valid ident, using a mix of Insp and
179
 // returns true if the given name is a valid ident, using a mix of Insp and
169
 // Chary's ident restrictions.
180
 // Chary's ident restrictions.
170
 func isIdent(name string) bool {
181
 func isIdent(name string) bool {

Loading…
Отказ
Запис