Browse Source

fix #1490

Track channel join times, use them to optionally enforce history access
restrictions
tags/v2.5.0-rc1
Shivaram Lingamneni 3 years ago
parent
commit
4a48e52518
12 changed files with 275 additions and 93 deletions
  1. 12
    7
      default.yaml
  2. 7
    2
      irc/accounts.go
  3. 73
    45
      irc/channel.go
  4. 25
    1
      irc/chanserv.go
  5. 7
    7
      irc/client.go
  6. 58
    3
      irc/config.go
  7. 51
    1
      irc/database.go
  8. 1
    1
      irc/getters.go
  9. 3
    3
      irc/handlers.go
  10. 15
    6
      irc/server.go
  11. 15
    13
      irc/types.go
  12. 8
    4
      traditional.yaml

+ 12
- 7
default.yaml View File

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

+ 7
- 2
irc/accounts.go View File

544
 	return err
544
 	return err
545
 }
545
 }
546
 
546
 
547
-func (am *AccountManager) saveChannels(account string, channelToModes map[string]string) {
547
+type alwaysOnChannelStatus struct {
548
+	Modes    string
549
+	JoinTime int64
550
+}
551
+
552
+func (am *AccountManager) saveChannels(account string, channelToModes map[string]alwaysOnChannelStatus) {
548
 	j, err := json.Marshal(channelToModes)
553
 	j, err := json.Marshal(channelToModes)
549
 	if err != nil {
554
 	if err != nil {
550
 		am.server.logger.Error("internal", "couldn't marshal channel-to-modes", account, err.Error())
555
 		am.server.logger.Error("internal", "couldn't marshal channel-to-modes", account, err.Error())
558
 	})
563
 	})
559
 }
564
 }
560
 
565
 
561
-func (am *AccountManager) loadChannels(account string) (channelToModes map[string]string) {
566
+func (am *AccountManager) loadChannels(account string) (channelToModes map[string]alwaysOnChannelStatus) {
562
 	key := fmt.Sprintf(keyAccountChannelToModes, account)
567
 	key := fmt.Sprintf(keyAccountChannelToModes, account)
563
 	var channelsStr string
568
 	var channelsStr string
564
 	am.server.store.View(func(tx *buntdb.Tx) error {
569
 	am.server.store.View(func(tx *buntdb.Tx) error {

+ 73
- 45
irc/channel.go View File

20
 )
20
 )
21
 
21
 
22
 type ChannelSettings struct {
22
 type ChannelSettings struct {
23
-	History HistoryStatus
23
+	History     HistoryStatus
24
+	QueryCutoff HistoryCutoff
24
 }
25
 }
25
 
26
 
26
 // Channel represents a channel that clients can join.
27
 // Channel represents a channel that clients can join.
109
 }
110
 }
110
 
111
 
111
 func (channel *Channel) resizeHistory(config *Config) {
112
 func (channel *Channel) resizeHistory(config *Config) {
112
-	status, _ := channel.historyStatus(config)
113
+	status, _, _ := channel.historyStatus(config)
113
 	if status == HistoryEphemeral {
114
 	if status == HistoryEphemeral {
114
 		channel.history.Resize(config.History.ChannelLength, time.Duration(config.History.AutoresizeWindow))
115
 		channel.history.Resize(config.History.ChannelLength, time.Duration(config.History.AutoresizeWindow))
115
 	} else {
116
 	} else {
443
 // Names sends the list of users joined to the channel to the given client.
444
 // Names sends the list of users joined to the channel to the given client.
444
 func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
445
 func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
445
 	channel.stateMutex.RLock()
446
 	channel.stateMutex.RLock()
446
-	clientModes, isJoined := channel.members[client]
447
+	clientData, isJoined := channel.members[client]
447
 	channel.stateMutex.RUnlock()
448
 	channel.stateMutex.RUnlock()
448
 	isOper := client.HasMode(modes.Operator)
449
 	isOper := client.HasMode(modes.Operator)
449
 	respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper &&
450
 	respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper &&
450
-		(!isJoined || clientModes.HighestChannelUserMode() == modes.Mode(0))
451
+		(!isJoined || clientData.modes.HighestChannelUserMode() == modes.Mode(0))
451
 	isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
452
 	isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
452
 	isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
453
 	isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
453
 
454
 
463
 				nick = target.Nick()
464
 				nick = target.Nick()
464
 			}
465
 			}
465
 			channel.stateMutex.RLock()
466
 			channel.stateMutex.RLock()
466
-			modeSet := channel.members[target]
467
+			memberData, _ := channel.members[target]
467
 			channel.stateMutex.RUnlock()
468
 			channel.stateMutex.RUnlock()
469
+			modeSet := memberData.modes
468
 			if modeSet == nil {
470
 			if modeSet == nil {
469
 				continue
471
 				continue
470
 			}
472
 			}
519
 // ClientIsAtLeast returns whether the client has at least the given channel privilege.
521
 // ClientIsAtLeast returns whether the client has at least the given channel privilege.
520
 func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool {
522
 func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool {
521
 	channel.stateMutex.RLock()
523
 	channel.stateMutex.RLock()
522
-	clientModes := channel.members[client]
524
+	memberData := channel.members[client]
523
 	founder := channel.registeredFounder
525
 	founder := channel.registeredFounder
524
 	channel.stateMutex.RUnlock()
526
 	channel.stateMutex.RUnlock()
525
 
527
 
528
 	}
530
 	}
529
 
531
 
530
 	for _, mode := range modes.ChannelUserModes {
532
 	for _, mode := range modes.ChannelUserModes {
531
-		if clientModes.HasMode(mode) {
533
+		if memberData.modes.HasMode(mode) {
532
 			return true
534
 			return true
533
 		}
535
 		}
534
 		if mode == permission {
536
 		if mode == permission {
541
 func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) string {
543
 func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) string {
542
 	channel.stateMutex.RLock()
544
 	channel.stateMutex.RLock()
543
 	defer channel.stateMutex.RUnlock()
545
 	defer channel.stateMutex.RUnlock()
544
-	modes, present := channel.members[client]
546
+	memberData, present := channel.members[client]
545
 	if !present {
547
 	if !present {
546
 		return ""
548
 		return ""
547
 	} else {
549
 	} else {
548
-		return modes.Prefixes(isMultiPrefix)
550
+		return memberData.modes.Prefixes(isMultiPrefix)
549
 	}
551
 	}
550
 }
552
 }
551
 
553
 
552
-func (channel *Channel) ClientStatus(client *Client) (present bool, cModes modes.Modes) {
554
+func (channel *Channel) ClientStatus(client *Client) (present bool, joinTimeSecs int64, cModes modes.Modes) {
553
 	channel.stateMutex.RLock()
555
 	channel.stateMutex.RLock()
554
 	defer channel.stateMutex.RUnlock()
556
 	defer channel.stateMutex.RUnlock()
555
-	modes, present := channel.members[client]
556
-	return present, modes.AllModes()
557
+	memberData, present := channel.members[client]
558
+	return present, time.Unix(0, memberData.joinTime).Unix(), memberData.modes.AllModes()
557
 }
559
 }
558
 
560
 
559
 // helper for persisting channel-user modes for always-on clients;
561
 // helper for persisting channel-user modes for always-on clients;
560
 // return the channel name and all channel-user modes for a client
562
 // return the channel name and all channel-user modes for a client
561
-func (channel *Channel) nameAndModes(client *Client) (chname string, modeStr string) {
563
+func (channel *Channel) alwaysOnStatus(client *Client) (chname string, status alwaysOnChannelStatus) {
562
 	channel.stateMutex.RLock()
564
 	channel.stateMutex.RLock()
563
 	defer channel.stateMutex.RUnlock()
565
 	defer channel.stateMutex.RUnlock()
564
 	chname = channel.name
566
 	chname = channel.name
565
-	modeStr = channel.members[client].String()
567
+	data := channel.members[client]
568
+	status.Modes = data.modes.String()
569
+	status.JoinTime = data.joinTime
566
 	return
570
 	return
567
 }
571
 }
568
 
572
 
569
 // overwrite any existing channel-user modes with the stored ones
573
 // overwrite any existing channel-user modes with the stored ones
570
-func (channel *Channel) setModesForClient(client *Client, modeStr string) {
574
+func (channel *Channel) setMemberStatus(client *Client, status alwaysOnChannelStatus) {
571
 	newModes := modes.NewModeSet()
575
 	newModes := modes.NewModeSet()
572
-	for _, mode := range modeStr {
576
+	for _, mode := range status.Modes {
573
 		newModes.SetMode(modes.Mode(mode), true)
577
 		newModes.SetMode(modes.Mode(mode), true)
574
 	}
578
 	}
575
 	channel.stateMutex.Lock()
579
 	channel.stateMutex.Lock()
577
 	if _, ok := channel.members[client]; !ok {
581
 	if _, ok := channel.members[client]; !ok {
578
 		return
582
 		return
579
 	}
583
 	}
580
-	channel.members[client] = newModes
584
+	memberData := channel.members[client]
585
+	memberData.modes = newModes
586
+	memberData.joinTime = status.JoinTime
587
+	channel.members[client] = memberData
581
 }
588
 }
582
 
589
 
583
 func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
590
 func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
584
 	channel.stateMutex.RLock()
591
 	channel.stateMutex.RLock()
585
 	founder := channel.registeredFounder
592
 	founder := channel.registeredFounder
586
-	clientModes := channel.members[client]
587
-	targetModes := channel.members[target]
593
+	clientModes := channel.members[client].modes
594
+	targetModes := channel.members[target].modes
588
 	channel.stateMutex.RUnlock()
595
 	channel.stateMutex.RUnlock()
589
 
596
 
590
 	if founder != "" {
597
 	if founder != "" {
612
 	channel.stateMutex.RLock()
619
 	channel.stateMutex.RLock()
613
 	defer channel.stateMutex.RUnlock()
620
 	defer channel.stateMutex.RUnlock()
614
 
621
 
615
-	isMember := hasPrivs || channel.members[client] != nil
622
+	isMember := hasPrivs || channel.members.Has(client)
616
 	showKey := isMember && (channel.key != "")
623
 	showKey := isMember && (channel.key != "")
617
 	showUserLimit := channel.userLimit > 0
624
 	showUserLimit := channel.userLimit > 0
618
 	showForward := channel.forward != ""
625
 	showForward := channel.forward != ""
660
 
667
 
661
 // figure out where history is being stored: persistent, ephemeral, or neither
668
 // figure out where history is being stored: persistent, ephemeral, or neither
662
 // target is only needed if we're doing persistent history
669
 // target is only needed if we're doing persistent history
663
-func (channel *Channel) historyStatus(config *Config) (status HistoryStatus, target string) {
670
+func (channel *Channel) historyStatus(config *Config) (status HistoryStatus, target string, restrictions HistoryCutoff) {
664
 	if !config.History.Enabled {
671
 	if !config.History.Enabled {
665
-		return HistoryDisabled, ""
672
+		return HistoryDisabled, "", HistoryCutoffNone
666
 	}
673
 	}
667
 
674
 
668
 	channel.stateMutex.RLock()
675
 	channel.stateMutex.RLock()
669
 	target = channel.nameCasefolded
676
 	target = channel.nameCasefolded
670
-	historyStatus := channel.settings.History
677
+	settings := channel.settings
671
 	registered := channel.registeredFounder != ""
678
 	registered := channel.registeredFounder != ""
672
 	channel.stateMutex.RUnlock()
679
 	channel.stateMutex.RUnlock()
673
 
680
 
674
-	return channelHistoryStatus(config, registered, historyStatus), target
681
+	restrictions = settings.QueryCutoff
682
+	if restrictions == HistoryCutoffDefault {
683
+		restrictions = config.History.Restrictions.queryCutoff
684
+	}
685
+
686
+	return channelHistoryStatus(config, registered, settings.History), target, restrictions
687
+}
688
+
689
+func (channel *Channel) joinTimeCutoff(client *Client) (present bool, cutoff time.Time) {
690
+	account := client.Account()
691
+
692
+	channel.stateMutex.RLock()
693
+	defer channel.stateMutex.RUnlock()
694
+	if data, ok := channel.members[client]; ok {
695
+		present = true
696
+		// report a cutoff of zero, i.e., no restriction, if the user is privileged
697
+		if !((account != "" && account == channel.registeredFounder) || data.modes.HasMode(modes.ChannelFounder) || data.modes.HasMode(modes.ChannelAdmin) || data.modes.HasMode(modes.ChannelOperator)) {
698
+			cutoff = time.Unix(0, data.joinTime)
699
+		}
700
+	}
701
+	return
675
 }
702
 }
676
 
703
 
677
 func channelHistoryStatus(config *Config, registered bool, storedStatus HistoryStatus) (result HistoryStatus) {
704
 func channelHistoryStatus(config *Config, registered bool, storedStatus HistoryStatus) (result HistoryStatus) {
697
 		return
724
 		return
698
 	}
725
 	}
699
 
726
 
700
-	status, target := channel.historyStatus(channel.server.Config())
727
+	status, target, _ := channel.historyStatus(channel.server.Config())
701
 	if status == HistoryPersistent {
728
 	if status == HistoryPersistent {
702
 		err = channel.server.historyDB.AddChannelItem(target, item, account)
729
 		err = channel.server.historyDB.AddChannelItem(target, item, account)
703
 	} else if status == HistoryEphemeral {
730
 	} else if status == HistoryEphemeral {
785
 				givenMode = persistentMode
812
 				givenMode = persistentMode
786
 			}
813
 			}
787
 			if givenMode != 0 {
814
 			if givenMode != 0 {
788
-				channel.members[client].SetMode(givenMode, true)
815
+				channel.members[client].modes.SetMode(givenMode, true)
789
 			}
816
 			}
790
 		}()
817
 		}()
791
 
818
 
825
 	for _, member := range channel.Members() {
852
 	for _, member := range channel.Members() {
826
 		if respectAuditorium {
853
 		if respectAuditorium {
827
 			channel.stateMutex.RLock()
854
 			channel.stateMutex.RLock()
828
-			memberModes, ok := channel.members[member]
855
+			memberData, ok := channel.members[member]
829
 			channel.stateMutex.RUnlock()
856
 			channel.stateMutex.RUnlock()
830
-			if !ok || memberModes.HighestChannelUserMode() == modes.Mode(0) {
857
+			if !ok || memberData.modes.HighestChannelUserMode() == modes.Mode(0) {
831
 				continue
858
 				continue
832
 			}
859
 			}
833
 		}
860
 		}
955
 func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) {
982
 func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) {
956
 	channel.stateMutex.RLock()
983
 	channel.stateMutex.RLock()
957
 	chname := channel.name
984
 	chname := channel.name
958
-	clientModes, ok := channel.members[client]
985
+	clientData, ok := channel.members[client]
959
 	channel.stateMutex.RUnlock()
986
 	channel.stateMutex.RUnlock()
960
 
987
 
961
 	if !ok {
988
 	if !ok {
974
 		params = append(params, message)
1001
 		params = append(params, message)
975
 	}
1002
 	}
976
 	respectAuditorium := channel.flags.HasMode(modes.Auditorium) &&
1003
 	respectAuditorium := channel.flags.HasMode(modes.Auditorium) &&
977
-		clientModes.HighestChannelUserMode() == modes.Mode(0)
1004
+		clientData.modes.HighestChannelUserMode() == modes.Mode(0)
978
 	var cache MessageCache
1005
 	var cache MessageCache
979
 	cache.Initialize(channel.server, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", params...)
1006
 	cache.Initialize(channel.server, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", params...)
980
 	for _, member := range channel.Members() {
1007
 	for _, member := range channel.Members() {
981
 		if respectAuditorium {
1008
 		if respectAuditorium {
982
 			channel.stateMutex.RLock()
1009
 			channel.stateMutex.RLock()
983
-			memberModes, ok := channel.members[member]
1010
+			memberData, ok := channel.members[member]
984
 			channel.stateMutex.RUnlock()
1011
 			channel.stateMutex.RUnlock()
985
-			if !ok || memberModes.HighestChannelUserMode() == modes.Mode(0) {
1012
+			if !ok || memberData.modes.HighestChannelUserMode() == modes.Mode(0) {
986
 				continue
1013
 				continue
987
 			}
1014
 			}
988
 		}
1015
 		}
1022
 
1049
 
1023
 func (channel *Channel) resumeAndAnnounce(session *Session) {
1050
 func (channel *Channel) resumeAndAnnounce(session *Session) {
1024
 	channel.stateMutex.RLock()
1051
 	channel.stateMutex.RLock()
1025
-	modeSet := channel.members[session.client]
1052
+	memberData, found := channel.members[session.client]
1026
 	channel.stateMutex.RUnlock()
1053
 	channel.stateMutex.RUnlock()
1027
-	if modeSet == nil {
1054
+	if !found {
1028
 		return
1055
 		return
1029
 	}
1056
 	}
1030
-	oldModes := modeSet.String()
1057
+	oldModes := memberData.modes.String()
1031
 	if 0 < len(oldModes) {
1058
 	if 0 < len(oldModes) {
1032
 		oldModes = "+" + oldModes
1059
 		oldModes = "+" + oldModes
1033
 	}
1060
 	}
1271
 // CanSpeak returns true if the client can speak on this channel, otherwise it returns false along with the channel mode preventing the client from speaking.
1298
 // CanSpeak returns true if the client can speak on this channel, otherwise it returns false along with the channel mode preventing the client from speaking.
1272
 func (channel *Channel) CanSpeak(client *Client) (bool, modes.Mode) {
1299
 func (channel *Channel) CanSpeak(client *Client) (bool, modes.Mode) {
1273
 	channel.stateMutex.RLock()
1300
 	channel.stateMutex.RLock()
1274
-	clientModes, hasClient := channel.members[client]
1301
+	memberData, hasClient := channel.members[client]
1275
 	channel.stateMutex.RUnlock()
1302
 	channel.stateMutex.RUnlock()
1303
+	clientModes := memberData.modes
1276
 
1304
 
1277
 	if !hasClient && channel.flags.HasMode(modes.NoOutside) {
1305
 	if !hasClient && channel.flags.HasMode(modes.NoOutside) {
1278
 		// TODO: enforce regular +b bans on -n channels?
1306
 		// TODO: enforce regular +b bans on -n channels?
1347
 
1375
 
1348
 	if channel.flags.HasMode(modes.OpModerated) {
1376
 	if channel.flags.HasMode(modes.OpModerated) {
1349
 		channel.stateMutex.RLock()
1377
 		channel.stateMutex.RLock()
1350
-		cuModes := channel.members[client]
1378
+		cuData := channel.members[client]
1351
 		channel.stateMutex.RUnlock()
1379
 		channel.stateMutex.RUnlock()
1352
-		if cuModes.HighestChannelUserMode() == modes.Mode(0) {
1380
+		if cuData.modes.HighestChannelUserMode() == modes.Mode(0) {
1353
 			// max(statusmsg_minmode, halfop)
1381
 			// max(statusmsg_minmode, halfop)
1354
 			if minPrefixMode == modes.Mode(0) || minPrefixMode == modes.Voice {
1382
 			if minPrefixMode == modes.Mode(0) || minPrefixMode == modes.Voice {
1355
 				minPrefixMode = modes.Halfop
1383
 				minPrefixMode = modes.Halfop
1402
 	change.Arg = target.Nick()
1430
 	change.Arg = target.Nick()
1403
 
1431
 
1404
 	channel.stateMutex.Lock()
1432
 	channel.stateMutex.Lock()
1405
-	modeset, exists := channel.members[target]
1433
+	memberData, exists := channel.members[target]
1406
 	if exists {
1434
 	if exists {
1407
-		if modeset.SetMode(change.Mode, change.Op == modes.Add) {
1435
+		if memberData.modes.SetMode(change.Mode, change.Op == modes.Add) {
1408
 			applied = true
1436
 			applied = true
1409
 			result = change
1437
 			result = change
1410
 		}
1438
 		}
1590
 	channel.stateMutex.RLock()
1618
 	channel.stateMutex.RLock()
1591
 	defer channel.stateMutex.RUnlock()
1619
 	defer channel.stateMutex.RUnlock()
1592
 
1620
 
1593
-	clientModes := channel.members[client]
1594
-	if clientModes == nil {
1621
+	clientData, found := channel.members[client]
1622
+	if !found {
1595
 		return // non-members have no friends
1623
 		return // non-members have no friends
1596
 	}
1624
 	}
1597
 	if !channel.flags.HasMode(modes.Auditorium) {
1625
 	if !channel.flags.HasMode(modes.Auditorium) {
1598
 		return channel.membersCache // default behavior for members
1626
 		return channel.membersCache // default behavior for members
1599
 	}
1627
 	}
1600
-	if clientModes.HighestChannelUserMode() != modes.Mode(0) {
1628
+	if clientData.modes.HighestChannelUserMode() != modes.Mode(0) {
1601
 		return channel.membersCache // +v and up can see everyone in the auditorium
1629
 		return channel.membersCache // +v and up can see everyone in the auditorium
1602
 	}
1630
 	}
1603
 	// without +v, your friends are those with +v and up
1631
 	// without +v, your friends are those with +v and up
1604
-	for member, memberModes := range channel.members {
1605
-		if memberModes.HighestChannelUserMode() != modes.Mode(0) {
1632
+	for member, memberData := range channel.members {
1633
+		if memberData.modes.HighestChannelUserMode() != modes.Mode(0) {
1606
 			friends = append(friends, member)
1634
 			friends = append(friends, member)
1607
 		}
1635
 		}
1608
 	}
1636
 	}

+ 25
- 1
irc/chanserv.go View File

171
 2. 'ephemeral'  [a limited amount of temporary history, not stored on disk]
171
 2. 'ephemeral'  [a limited amount of temporary history, not stored on disk]
172
 3. 'on'         [history stored in a permanent database, if available]
172
 3. 'on'         [history stored in a permanent database, if available]
173
 4. 'default'    [use the server default]`,
173
 4. 'default'    [use the server default]`,
174
+				`$bQUERY-CUTOFF$b
175
+'query-cutoff' lets you restrict how much channel history can be retrieved
176
+by unprivileged users. Your options are:
177
+1. 'none'               [no restrictions]
178
+2. 'registration-time'  [users can view history from after their account was
179
+                         registered, plus a grace period]
180
+3. 'join-time'          [users can biew history from after they joined the
181
+                         channel; note that history will be effectively
182
+                         unavailable to clients that are not always-on]
183
+4. 'default'            [use the server default]`,
174
 			},
184
 			},
175
 			enabled:   chanregEnabled,
185
 			enabled:   chanregEnabled,
176
 			minParams: 3,
186
 			minParams: 3,
329
 		target = client
339
 		target = client
330
 	}
340
 	}
331
 
341
 
332
-	present, cumodes := channel.ClientStatus(target)
342
+	present, _, cumodes := channel.ClientStatus(target)
333
 	if !present || len(cumodes) == 0 {
343
 	if !present || len(cumodes) == 0 {
334
 		service.Notice(rb, client.t("Target has no privileges to remove"))
344
 		service.Notice(rb, client.t("Target has no privileges to remove"))
335
 		return
345
 		return
745
 		effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
755
 		effectiveValue := historyEnabled(config.History.Persistent.RegisteredChannels, settings.History)
746
 		service.Notice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
756
 		service.Notice(rb, fmt.Sprintf(client.t("The stored channel history setting is: %s"), historyStatusToString(settings.History)))
747
 		service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
757
 		service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history setting is: %s"), historyStatusToString(effectiveValue)))
758
+	case "query-cutoff":
759
+		effectiveValue := settings.QueryCutoff
760
+		if effectiveValue == HistoryCutoffDefault {
761
+			effectiveValue = config.History.Restrictions.queryCutoff
762
+		}
763
+		service.Notice(rb, fmt.Sprintf(client.t("The stored channel history query cutoff setting is: %s"), historyCutoffToString(settings.QueryCutoff)))
764
+		service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history query cutoff setting is: %s"), historyCutoffToString(effectiveValue)))
748
 	default:
765
 	default:
749
 		service.Notice(rb, client.t("Invalid params"))
766
 		service.Notice(rb, client.t("Invalid params"))
750
 	}
767
 	}
788
 		}
805
 		}
789
 		channel.SetSettings(settings)
806
 		channel.SetSettings(settings)
790
 		channel.resizeHistory(server.Config())
807
 		channel.resizeHistory(server.Config())
808
+	case "query-cutoff":
809
+		settings.QueryCutoff, err = historyCutoffFromString(value)
810
+		if err != nil {
811
+			err = errInvalidParams
812
+			break
813
+		}
814
+		channel.SetSettings(settings)
791
 	}
815
 	}
792
 
816
 
793
 	switch err {
817
 	switch err {

+ 7
- 7
irc/client.go View File

407
 	client.run(session)
407
 	client.run(session)
408
 }
408
 }
409
 
409
 
410
-func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToModes map[string]string, lastSeen map[string]time.Time, uModes modes.Modes, realname string) {
410
+func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus map[string]alwaysOnChannelStatus, lastSeen map[string]time.Time, uModes modes.Modes, realname string) {
411
 	now := time.Now().UTC()
411
 	now := time.Now().UTC()
412
 	config := server.Config()
412
 	config := server.Config()
413
 	if lastSeen == nil && account.Settings.AutoreplayMissed {
413
 	if lastSeen == nil && account.Settings.AutoreplayMissed {
471
 	// XXX set this last to avoid confusing SetNick:
471
 	// XXX set this last to avoid confusing SetNick:
472
 	client.registered = true
472
 	client.registered = true
473
 
473
 
474
-	for chname, modeStr := range channelToModes {
474
+	for chname, status := range channelToStatus {
475
 		// XXX we're using isSajoin=true, to make these joins succeed even without channel key
475
 		// XXX we're using isSajoin=true, to make these joins succeed even without channel key
476
 		// this is *probably* ok as long as the persisted memberships are accurate
476
 		// this is *probably* ok as long as the persisted memberships are accurate
477
 		server.channels.Join(client, chname, "", true, nil)
477
 		server.channels.Join(client, chname, "", true, nil)
478
 		if channel := server.channels.Get(chname); channel != nil {
478
 		if channel := server.channels.Get(chname); channel != nil {
479
-			channel.setModesForClient(client, modeStr)
479
+			channel.setMemberStatus(client, status)
480
 		} else {
480
 		} else {
481
 			server.logger.Error("internal", "could not create channel", chname)
481
 			server.logger.Error("internal", "could not create channel", chname)
482
 		}
482
 		}
966
 		for _, member := range channel.auditoriumFriends(client) {
966
 		for _, member := range channel.auditoriumFriends(client) {
967
 			friends.Add(member)
967
 			friends.Add(member)
968
 		}
968
 		}
969
-		status, _ := channel.historyStatus(config)
969
+		status, _, _ := channel.historyStatus(config)
970
 		if status == HistoryEphemeral {
970
 		if status == HistoryEphemeral {
971
 			lastDiscarded := channel.history.LastDiscarded()
971
 			lastDiscarded := channel.history.LastDiscarded()
972
 			if oldestLostMessage.Before(lastDiscarded) {
972
 			if oldestLostMessage.Before(lastDiscarded) {
2000
 
2000
 
2001
 	if (dirtyBits & IncludeChannels) != 0 {
2001
 	if (dirtyBits & IncludeChannels) != 0 {
2002
 		channels := client.Channels()
2002
 		channels := client.Channels()
2003
-		channelToModes := make(map[string]string, len(channels))
2003
+		channelToModes := make(map[string]alwaysOnChannelStatus, len(channels))
2004
 		for _, channel := range channels {
2004
 		for _, channel := range channels {
2005
-			chname, modes := channel.nameAndModes(client)
2006
-			channelToModes[chname] = modes
2005
+			chname, status := channel.alwaysOnStatus(client)
2006
+			channelToModes[chname] = status
2007
 		}
2007
 		}
2008
 		client.server.accounts.saveChannels(account, channelToModes)
2008
 		client.server.accounts.saveChannels(account, channelToModes)
2009
 	}
2009
 	}

+ 58
- 3
irc/config.go View File

62
 	HideSTS   bool `yaml:"hide-sts"`
62
 	HideSTS   bool `yaml:"hide-sts"`
63
 }
63
 }
64
 
64
 
65
+type HistoryCutoff uint
66
+
67
+const (
68
+	HistoryCutoffDefault HistoryCutoff = iota
69
+	HistoryCutoffNone
70
+	HistoryCutoffRegistrationTime
71
+	HistoryCutoffJoinTime
72
+)
73
+
74
+func historyCutoffToString(restriction HistoryCutoff) string {
75
+	switch restriction {
76
+	case HistoryCutoffDefault:
77
+		return "default"
78
+	case HistoryCutoffNone:
79
+		return "none"
80
+	case HistoryCutoffRegistrationTime:
81
+		return "registration-time"
82
+	case HistoryCutoffJoinTime:
83
+		return "join-time"
84
+	default:
85
+		return ""
86
+	}
87
+}
88
+
89
+func historyCutoffFromString(str string) (result HistoryCutoff, err error) {
90
+	switch strings.ToLower(str) {
91
+	case "default":
92
+		return HistoryCutoffDefault, nil
93
+	case "none", "disabled", "off", "false":
94
+		return HistoryCutoffNone, nil
95
+	case "registration-time":
96
+		return HistoryCutoffRegistrationTime, nil
97
+	case "join-time":
98
+		return HistoryCutoffJoinTime, nil
99
+	default:
100
+		return HistoryCutoffDefault, errInvalidParams
101
+	}
102
+}
103
+
65
 type PersistentStatus uint
104
 type PersistentStatus uint
66
 
105
 
67
 const (
106
 const (
615
 		ChathistoryMax   int              `yaml:"chathistory-maxmessages"`
654
 		ChathistoryMax   int              `yaml:"chathistory-maxmessages"`
616
 		ZNCMax           int              `yaml:"znc-maxmessages"`
655
 		ZNCMax           int              `yaml:"znc-maxmessages"`
617
 		Restrictions     struct {
656
 		Restrictions     struct {
618
-			ExpireTime              custime.Duration `yaml:"expire-time"`
619
-			EnforceRegistrationDate bool             `yaml:"enforce-registration-date"`
620
-			GracePeriod             custime.Duration `yaml:"grace-period"`
657
+			ExpireTime custime.Duration `yaml:"expire-time"`
658
+			// legacy key, superceded by QueryCutoff:
659
+			EnforceRegistrationDate_ bool   `yaml:"enforce-registration-date"`
660
+			QueryCutoff              string `yaml:"query-cutoff"`
661
+			queryCutoff              HistoryCutoff
662
+			GracePeriod              custime.Duration `yaml:"grace-period"`
621
 		}
663
 		}
622
 		Persistent struct {
664
 		Persistent struct {
623
 			Enabled              bool
665
 			Enabled              bool
1354
 		config.History.ZNCMax = config.History.ChathistoryMax
1396
 		config.History.ZNCMax = config.History.ChathistoryMax
1355
 	}
1397
 	}
1356
 
1398
 
1399
+	if config.History.Restrictions.QueryCutoff != "" {
1400
+		config.History.Restrictions.queryCutoff, err = historyCutoffFromString(config.History.Restrictions.QueryCutoff)
1401
+		if err != nil {
1402
+			return nil, fmt.Errorf("invalid value of history.query-restrictions: %w", err)
1403
+		}
1404
+	} else {
1405
+		if config.History.Restrictions.EnforceRegistrationDate_ {
1406
+			config.History.Restrictions.queryCutoff = HistoryCutoffRegistrationTime
1407
+		} else {
1408
+			config.History.Restrictions.queryCutoff = HistoryCutoffNone
1409
+		}
1410
+	}
1411
+
1357
 	config.Roleplay.addSuffix = utils.BoolDefaultTrue(config.Roleplay.AddSuffix)
1412
 	config.Roleplay.addSuffix = utils.BoolDefaultTrue(config.Roleplay.AddSuffix)
1358
 
1413
 
1359
 	config.Datastore.MySQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime)
1414
 	config.Datastore.MySQL.ExpireTime = time.Duration(config.History.Restrictions.ExpireTime)

+ 51
- 1
irc/database.go View File

24
 	// 'version' of the database schema
24
 	// 'version' of the database schema
25
 	keySchemaVersion = "db.version"
25
 	keySchemaVersion = "db.version"
26
 	// latest schema of the db
26
 	// latest schema of the db
27
-	latestDbSchema = 19
27
+	latestDbSchema = 20
28
 
28
 
29
 	keyCloakSecret = "crypto.cloak_secret"
29
 	keyCloakSecret = "crypto.cloak_secret"
30
 )
30
 )
963
 	return nil
963
 	return nil
964
 }
964
 }
965
 
965
 
966
+// #1490: start tracking join times for always-on clients
967
+func schemaChangeV19To20(config *Config, tx *buntdb.Tx) error {
968
+	type joinData struct {
969
+		Modes    string
970
+		JoinTime int64
971
+	}
972
+
973
+	var accounts []string
974
+	var data []string
975
+
976
+	now := time.Now().UnixNano()
977
+
978
+	prefix := "account.channeltomodes "
979
+	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
980
+		if !strings.HasPrefix(key, prefix) {
981
+			return false
982
+		}
983
+		accounts = append(accounts, strings.TrimPrefix(key, prefix))
984
+		data = append(data, value)
985
+		return true
986
+	})
987
+
988
+	for i, account := range accounts {
989
+		var existingMap map[string]string
990
+		err := json.Unmarshal([]byte(data[i]), &existingMap)
991
+		if err != nil {
992
+			return err
993
+		}
994
+		newMap := make(map[string]joinData)
995
+		for channel, modeStr := range existingMap {
996
+			newMap[channel] = joinData{
997
+				Modes:    modeStr,
998
+				JoinTime: now,
999
+			}
1000
+		}
1001
+		serialized, err := json.Marshal(newMap)
1002
+		if err != nil {
1003
+			return err
1004
+		}
1005
+		tx.Set(prefix+account, string(serialized), nil)
1006
+	}
1007
+
1008
+	return nil
1009
+}
1010
+
966
 func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
1011
 func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
967
 	for _, change := range allChanges {
1012
 	for _, change := range allChanges {
968
 		if initialVersion == change.InitialVersion {
1013
 		if initialVersion == change.InitialVersion {
1063
 		TargetVersion:  19,
1108
 		TargetVersion:  19,
1064
 		Changer:        schemaChangeV18To19,
1109
 		Changer:        schemaChangeV18To19,
1065
 	},
1110
 	},
1111
+	{
1112
+		InitialVersion: 19,
1113
+		TargetVersion:  20,
1114
+		Changer:        schemaChangeV19To20,
1115
+	},
1066
 }
1116
 }

+ 1
- 1
irc/getters.go View File

522
 
522
 
523
 func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
523
 func (channel *Channel) HighestUserMode(client *Client) (result modes.Mode) {
524
 	channel.stateMutex.RLock()
524
 	channel.stateMutex.RLock()
525
-	clientModes := channel.members[client]
525
+	clientModes := channel.members[client].modes
526
 	channel.stateMutex.RUnlock()
526
 	channel.stateMutex.RUnlock()
527
 	return clientModes.HighestChannelUserMode()
527
 	return clientModes.HighestChannelUserMode()
528
 }
528
 }

+ 3
- 3
irc/handlers.go View File

985
 		claims["channel"] = channel.Name()
985
 		claims["channel"] = channel.Name()
986
 		claims["joined"] = 0
986
 		claims["joined"] = 0
987
 		claims["cmodes"] = []string{}
987
 		claims["cmodes"] = []string{}
988
-		if present, cModes := channel.ClientStatus(client); present {
989
-			claims["joined"] = 1
988
+		if present, joinTimeSecs, cModes := channel.ClientStatus(client); present {
989
+			claims["joined"] = joinTimeSecs
990
 			var modeStrings []string
990
 			var modeStrings []string
991
 			for _, cMode := range cModes {
991
 			for _, cMode := range cModes {
992
 				modeStrings = append(modeStrings, string(cMode))
992
 				modeStrings = append(modeStrings, string(cMode))
2649
 	}
2649
 	}
2650
 
2650
 
2651
 	config := server.Config()
2651
 	config := server.Config()
2652
-	status, _ := channel.historyStatus(config)
2652
+	status, _, _ := channel.historyStatus(config)
2653
 	if status == HistoryPersistent {
2653
 	if status == HistoryPersistent {
2654
 		rb.Add(nil, server.name, "FAIL", "RENAME", "CANNOT_RENAME", oldName, utils.SafeErrorParam(newName), client.t("Channels with persistent history cannot be renamed"))
2654
 		rb.Add(nil, server.name, "FAIL", "RENAME", "CANNOT_RENAME", oldName, utils.SafeErrorParam(newName), client.t("Channels with persistent history cannot be renamed"))
2655
 		return false
2655
 		return false

+ 15
- 6
irc/server.go View File

850
 	var status HistoryStatus
850
 	var status HistoryStatus
851
 	var target, correspondent string
851
 	var target, correspondent string
852
 	var hist *history.Buffer
852
 	var hist *history.Buffer
853
+	restriction := HistoryCutoffNone
853
 	channel = providedChannel
854
 	channel = providedChannel
854
 	if channel == nil {
855
 	if channel == nil {
855
 		if strings.HasPrefix(query, "#") {
856
 		if strings.HasPrefix(query, "#") {
859
 			}
860
 			}
860
 		}
861
 		}
861
 	}
862
 	}
863
+	var joinTimeCutoff time.Time
862
 	if channel != nil {
864
 	if channel != nil {
863
-		if !channel.hasClient(client) {
865
+		if present, cutoff := channel.joinTimeCutoff(client); present {
866
+			joinTimeCutoff = cutoff
867
+		} else {
864
 			err = errInsufficientPrivs
868
 			err = errInsufficientPrivs
865
 			return
869
 			return
866
 		}
870
 		}
867
-		status, target = channel.historyStatus(config)
871
+		status, target, restriction = channel.historyStatus(config)
868
 		switch status {
872
 		switch status {
869
 		case HistoryEphemeral:
873
 		case HistoryEphemeral:
870
 			hist = &channel.history
874
 			hist = &channel.history
896
 		cutoff = time.Now().UTC().Add(-time.Duration(config.History.Restrictions.ExpireTime))
900
 		cutoff = time.Now().UTC().Add(-time.Duration(config.History.Restrictions.ExpireTime))
897
 	}
901
 	}
898
 	// #836: registration date cutoff is always enforced for DMs
902
 	// #836: registration date cutoff is always enforced for DMs
899
-	if config.History.Restrictions.EnforceRegistrationDate || channel == nil {
903
+	// either way, take the later of the two cutoffs
904
+	if restriction == HistoryCutoffRegistrationTime || channel == nil {
900
 		regCutoff := client.historyCutoff()
905
 		regCutoff := client.historyCutoff()
901
-		// take the later of the two cutoffs
902
 		if regCutoff.After(cutoff) {
906
 		if regCutoff.After(cutoff) {
903
 			cutoff = regCutoff
907
 			cutoff = regCutoff
904
 		}
908
 		}
909
+	} else if restriction == HistoryCutoffJoinTime {
910
+		if joinTimeCutoff.After(cutoff) {
911
+			cutoff = joinTimeCutoff
912
+		}
905
 	}
913
 	}
914
+
906
 	// #836 again: grace period is never applied to DMs
915
 	// #836 again: grace period is never applied to DMs
907
-	if !cutoff.IsZero() && channel != nil {
916
+	if !cutoff.IsZero() && channel != nil && restriction != HistoryCutoffJoinTime {
908
 		cutoff = cutoff.Add(-time.Duration(config.History.Restrictions.GracePeriod))
917
 		cutoff = cutoff.Add(-time.Duration(config.History.Restrictions.GracePeriod))
909
 	}
918
 	}
910
 
919
 
958
 		if target[0] == '#' {
967
 		if target[0] == '#' {
959
 			channel := server.channels.Get(target)
968
 			channel := server.channels.Get(target)
960
 			if channel != nil {
969
 			if channel != nil {
961
-				if status, _ := channel.historyStatus(config); status == HistoryEphemeral {
970
+				if status, _, _ := channel.historyStatus(config); status == HistoryEphemeral {
962
 					hist = &channel.history
971
 					hist = &channel.history
963
 				}
972
 				}
964
 			}
973
 			}

+ 15
- 13
irc/types.go View File

5
 
5
 
6
 package irc
6
 package irc
7
 
7
 
8
-import "github.com/oragono/oragono/irc/modes"
8
+import (
9
+	"time"
10
+
11
+	"github.com/oragono/oragono/irc/modes"
12
+)
9
 
13
 
10
 type empty struct{}
14
 type empty struct{}
11
 
15
 
28
 	return ok
32
 	return ok
29
 }
33
 }
30
 
34
 
35
+type memberData struct {
36
+	modes    *modes.ModeSet
37
+	joinTime int64
38
+}
39
+
31
 // MemberSet is a set of members with modes.
40
 // MemberSet is a set of members with modes.
32
-type MemberSet map[*Client]*modes.ModeSet
41
+type MemberSet map[*Client]memberData
33
 
42
 
34
 // Add adds the given client to this set.
43
 // Add adds the given client to this set.
35
 func (members MemberSet) Add(member *Client) {
44
 func (members MemberSet) Add(member *Client) {
36
-	members[member] = modes.NewModeSet()
45
+	members[member] = memberData{
46
+		modes:    modes.NewModeSet(),
47
+		joinTime: time.Now().UnixNano(),
48
+	}
37
 }
49
 }
38
 
50
 
39
 // Remove removes the given client from this set.
51
 // Remove removes the given client from this set.
47
 	return ok
59
 	return ok
48
 }
60
 }
49
 
61
 
50
-// AnyHasMode returns true if any of our clients has the given mode.
51
-func (members MemberSet) AnyHasMode(mode modes.Mode) bool {
52
-	for _, modes := range members {
53
-		if modes.HasMode(mode) {
54
-			return true
55
-		}
56
-	}
57
-	return false
58
-}
59
-
60
 // ChannelSet is a set of channels.
62
 // ChannelSet is a set of channels.
61
 type ChannelSet map[*Channel]empty
63
 type ChannelSet map[*Channel]empty

+ 8
- 4
traditional.yaml View File

851
         # (and will eventually be deleted from persistent storage, if that's enabled)
851
         # (and will eventually be deleted from persistent storage, if that's enabled)
852
         expire-time: 1w
852
         expire-time: 1w
853
 
853
 
854
-        # if this is set, logged-in users cannot retrieve messages older than their
855
-        # account registration date, and logged-out users cannot retrieve messages
856
-        # older than their sign-on time (modulo grace-period, see below):
857
-        enforce-registration-date: false
854
+        # this restricts access to channel history (it can be overridden by channel
855
+        # owners). options are: 'none' (no restrictions), 'registration-time'
856
+        # (logged-in users cannot retrieve messages older than their account
857
+        # registration date, and anonymous users cannot retrieve messages older than
858
+        # their sign-on time, modulo the grace-period described below), and
859
+        # 'join-time' (users cannot retrieve messages older than the time they
860
+        # joined the channel, so only always-on clients can view history).
861
+        query-cutoff: 'none'
858
 
862
 
859
         # but if this is set, you can retrieve messages that are up to `grace-period`
863
         # but if this is set, you can retrieve messages that are up to `grace-period`
860
         # older than the above cutoff time. this is recommended to allow logged-out
864
         # older than the above cutoff time. this is recommended to allow logged-out

Loading…
Cancel
Save