Просмотр исходного кода

Revert "issue #41 - warning level decay + rate limit scaling"

This reverts commit 497e00df21cbd49ee7d22f6c188f08e5a29dde14.
Mike 10 месяцев назад
Родитель
Сommit
b87afdc49e

+ 25 - 20
cmd/server/factory.go

@@ -29,7 +29,6 @@ type Container struct {
 	cfg                    config.Config
 	chatSessionManager     *state.InMemoryChatSessionManager
 	hmacCookieBaker        state.HMACCookieBaker
-	icbmSvc                *foodgroup.ICBMService
 	inMemorySessionManager *state.InMemorySessionManager
 	logger                 *slog.Logger
 	rateLimitClasses       wire.RateLimitClasses
@@ -75,19 +74,6 @@ func MakeCommonDeps() (Container, error) {
 	c.chatSessionManager = state.NewInMemoryChatSessionManager(c.logger)
 	c.rateLimitClasses = wire.DefaultRateLimitClasses()
 	c.snacRateLimits = wire.DefaultSNACRateLimits()
-
-	// ICBM svc is a common dep because OSCAR and TOC need to share convo
-	// history state.
-	c.icbmSvc = foodgroup.NewICBMService(
-		c.sqLiteUserStore,
-		c.inMemorySessionManager,
-		c.sqLiteUserStore,
-		c.sqLiteUserStore,
-		c.inMemorySessionManager,
-		c.snacRateLimits,
-		c.logger,
-	)
-
 	return c, nil
 }
 
@@ -263,6 +249,14 @@ func OSCAR(deps Container) *oscar.Server {
 		deps.inMemorySessionManager,
 		deps.inMemorySessionManager,
 	)
+	icbmService := foodgroup.NewICBMService(
+		deps.sqLiteUserStore,
+		deps.inMemorySessionManager,
+		deps.sqLiteUserStore,
+		deps.sqLiteUserStore,
+		deps.inMemorySessionManager,
+		deps.snacRateLimits,
+	)
 	icqService := foodgroup.NewICQService(deps.inMemorySessionManager, deps.sqLiteUserStore, deps.sqLiteUserStore,
 		logger, deps.inMemorySessionManager, deps.sqLiteUserStore)
 	locateService := foodgroup.NewLocateService(
@@ -307,7 +301,7 @@ func OSCAR(deps Container) *oscar.Server {
 			ChatNavService:    chatNavService,
 			ChatService:       chatService,
 			FeedbagService:    feedbagService,
-			ICBMService:       deps.icbmSvc,
+			ICBMService:       icbmService,
 			ICQService:        icqService,
 			LocateService:     locateService,
 			ODirService:       oDirService,
@@ -323,7 +317,6 @@ func OSCAR(deps Container) *oscar.Server {
 		deps.snacRateLimits,
 		oscar.NewIPRateLimiter(rate.Every(1*time.Minute), 10, 1*time.Minute),
 		deps.Listeners,
-		deps.icbmSvc.DecayWarnLevel,
 	)
 }
 
@@ -350,7 +343,6 @@ func MgmtAPI(deps Container) *http.Server {
 // TOC creates a TOC server.
 func TOC(deps Container) *toc.Server {
 	logger := deps.logger.With("svc", "TOC")
-
 	return toc.NewServer(
 		deps.cfg.TOCListeners,
 		logger,
@@ -384,7 +376,14 @@ func TOC(deps Container) *toc.Server {
 			),
 			CookieBaker:      deps.hmacCookieBaker,
 			DirSearchService: foodgroup.NewODirService(logger, deps.sqLiteUserStore),
-			ICBMService:      deps.icbmSvc,
+			ICBMService: foodgroup.NewICBMService(
+				deps.sqLiteUserStore,
+				deps.inMemorySessionManager,
+				deps.sqLiteUserStore,
+				deps.sqLiteUserStore,
+				deps.inMemorySessionManager,
+				deps.snacRateLimits,
+			),
 			LocateService: foodgroup.NewLocateService(
 				deps.sqLiteUserStore,
 				deps.inMemorySessionManager,
@@ -420,7 +419,6 @@ func TOC(deps Container) *toc.Server {
 			HTTPIPRateLimiter: toc.NewIPRateLimiter(rate.Every(1*time.Minute), 10, 1*time.Minute),
 		},
 		toc.NewIPRateLimiter(rate.Every(1*time.Minute), 10, 1*time.Minute),
-		deps.icbmSvc.DecayWarnLevel,
 	)
 }
 
@@ -457,7 +455,14 @@ func WebAPI(deps Container) *webapi.Server {
 		),
 		CookieBaker:      deps.hmacCookieBaker,
 		DirSearchService: foodgroup.NewODirService(logger, deps.sqLiteUserStore),
-		ICBMService:      deps.icbmSvc,
+		ICBMService: foodgroup.NewICBMService(
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+			deps.sqLiteUserStore,
+			deps.sqLiteUserStore,
+			deps.inMemorySessionManager,
+			deps.snacRateLimits,
+		),
 		LocateService: foodgroup.NewLocateService(
 			deps.sqLiteUserStore,
 			deps.inMemorySessionManager,

+ 0 - 4
foodgroup/buddy.go

@@ -104,10 +104,6 @@ func (s BuddyService) BroadcastBuddyDeparted(ctx context.Context, sess *state.Se
 	return s.buddyBroadcaster.BroadcastBuddyDeparted(ctx, sess)
 }
 
-func (s BuddyService) BroadcastBuddyArrived(ctx context.Context, sess *state.Session) error {
-	return s.buddyBroadcaster.BroadcastBuddyArrived(ctx, sess)
-}
-
 func newBuddyNotifier(
 	buddyIconManager BuddyIconManager,
 	relationshipFetcher RelationshipFetcher,

+ 2 - 2
foodgroup/helpers_test.go

@@ -719,9 +719,9 @@ type createChatRoomParams []struct {
 }
 
 // sessOptWarning sets a warning level on the session object
-func sessOptWarning(level int16) func(session *state.Session) {
+func sessOptWarning(level uint16) func(session *state.Session) {
 	return func(session *state.Session) {
-		session.IncrementWarning(level, 1)
+		session.IncrementWarning(level)
 	}
 }
 

+ 3 - 211
foodgroup/icbm.go

@@ -4,19 +4,15 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"log/slog"
 	"time"
 
-	"github.com/patrickmn/go-cache"
-
 	"github.com/mk6i/retro-aim-server/state"
 	"github.com/mk6i/retro-aim-server/wire"
 )
 
 const (
-	evilDelta       = uint16(100)
-	evilDeltaAnon   = uint16(30)
-	warningDecayPct = -50
+	evilDelta     = uint16(100)
+	evilDeltaAnon = uint16(30)
 )
 
 // NewICBMService returns a new instance of ICBMService.
@@ -27,7 +23,6 @@ func NewICBMService(
 	relationshipFetcher RelationshipFetcher,
 	sessionRetriever SessionRetriever,
 	snacRateLimits wire.SNACRateLimits,
-	logger *slog.Logger,
 ) *ICBMService {
 	return &ICBMService{
 		relationshipFetcher: relationshipFetcher,
@@ -37,9 +32,6 @@ func NewICBMService(
 		timeNow:             time.Now,
 		sessionRetriever:    sessionRetriever,
 		snacRateLimits:      snacRateLimits,
-		convoTracker:        newConvoTracker(),
-		logger:              logger,
-		interval:            5 * time.Minute,
 	}
 }
 
@@ -54,9 +46,6 @@ type ICBMService struct {
 	timeNow             func() time.Time
 	sessionRetriever    SessionRetriever
 	snacRateLimits      wire.SNACRateLimits
-	convoTracker        *convoTracker
-	logger              *slog.Logger
-	interval            time.Duration
 }
 
 // ParameterQuery returns ICBM service parameters.
@@ -171,8 +160,6 @@ func (s ICBMService) ChannelMsgToHost(ctx context.Context, sess *state.Session,
 		Body: clientIM,
 	})
 
-	s.convoTracker.trackConvo(time.Now(), sess.IdentScreenName(), recipSess.IdentScreenName())
-
 	if _, requestedConfirmation := inBody.TLVRestBlock.Bytes(wire.ICBMTLVRequestHostAck); !requestedConfirmation {
 		// don't ack message
 		return nil, nil
@@ -323,44 +310,11 @@ func (s ICBMService) EvilRequest(ctx context.Context, sess *state.Session, inFra
 		}, nil
 	}
 
-	canWarn := s.convoTracker.trackWarn(time.Now(), sess.IdentScreenName(), recipSess.IdentScreenName())
-	if !canWarn {
-		return wire.SNACMessage{
-			Frame: wire.SNACFrame{
-				FoodGroup: wire.ICBM,
-				SubGroup:  wire.ICBMErr,
-				RequestID: inFrame.RequestID,
-			},
-			Body: wire.SNACError{
-				Code: wire.ErrorCodeRequestDenied,
-			},
-		}, nil
-	}
-
 	increase := evilDelta
 	if inBody.SendAs == 1 {
 		increase = evilDeltaAnon
 	}
-
-	// get the rate class for sending IMs, which gets limited when the user gets warned
-	classID, ok := s.snacRateLimits.RateClassLookup(wire.ICBM, wire.ICBMChannelMsgToHost)
-	if !ok {
-		panic("failed to retrieve rate class for ICBMChannelMsgToHost")
-	}
-
-	if ok := recipSess.IncrementWarning(int16(increase), classID); !ok {
-		return wire.SNACMessage{
-			Frame: wire.SNACFrame{
-				FoodGroup: wire.ICBM,
-				SubGroup:  wire.ICBMErr,
-				RequestID: inFrame.RequestID,
-			},
-			Body: wire.SNACError{
-				Code: wire.ErrorCodeRequestDenied,
-			},
-		}, nil
-	}
-	recipSess.NotifyWarning(ctx)
+	recipSess.IncrementWarning(increase)
 
 	notif := wire.SNAC_0x01_0x10_OServiceEvilNotification{
 		NewEvil: recipSess.Warning(),
@@ -403,165 +357,3 @@ func (s ICBMService) EvilRequest(ctx context.Context, sess *state.Session, inFra
 		},
 	}, nil
 }
-
-// DecayWarnLevel gradually reduces a user's warning level over time.
-// It listens for warning notifications and starts a periodic decay process
-// that reduces the warning level by a fixed percentage at regular intervals
-// until the warning level reaches zero. Warning updates are broadcast to
-// users who have this user on their buddy list.
-func (s ICBMService) DecayWarnLevel(ctx context.Context, sess *state.Session) {
-	var inProgress bool
-	var ticker *time.Ticker
-	var tickC <-chan time.Time // nil when idle, enables/disables the select case
-
-	stopTicker := func() {
-		if ticker != nil {
-			ticker.Stop()
-			ticker = nil
-		}
-		tickC = nil
-		inProgress = false
-	}
-
-	startTicker := func() {
-		ticker = time.NewTicker(s.interval)
-		tickC = ticker.C
-		inProgress = true
-		s.logger.DebugContext(ctx, "warning decay started")
-	}
-
-	// get the rate class for sending IMs, which gets limited when the user gets warned
-	classID, ok := s.snacRateLimits.RateClassLookup(wire.ICBM, wire.ICBMChannelMsgToHost)
-	if !ok {
-		panic("failed to retrieve rate class for ICBMChannelMsgToHost")
-	}
-
-	for {
-		select {
-		case <-ctx.Done():
-			stopTicker()
-			return
-
-		case <-sess.WarningCh():
-			if inProgress {
-				s.logger.DebugContext(ctx, "warning decay already in progress")
-				continue
-			}
-			startTicker()
-
-		case <-tickC:
-			sess.IncrementWarning(warningDecayPct, classID)
-
-			if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, sess); err != nil {
-				s.logger.ErrorContext(ctx, "BroadcastBuddyArrived failed", "err", err)
-			} else {
-				s.logger.DebugContext(ctx, "warning lowered", "remaining", sess.Warning())
-			}
-
-			if sess.Warning() <= 0 {
-				s.logger.DebugContext(ctx, "warning decay complete")
-				stopTicker()
-			}
-		}
-	}
-}
-
-// convoTracker keeps track of messages initiated from a sender to a recipient.
-// A user (the warner) can only warn another user (the warnee) only if the
-// warner has received a message from the warnee. The warner may only warn 1
-// time per message received from warnee. The warner may only warn the warnee
-// up to 3 times per warn window.
-type convoTracker struct {
-	convos *cache.Cache
-	warns  *cache.Cache
-	window time.Duration
-}
-
-func newConvoTracker() *convoTracker {
-	window := 1 * time.Hour
-	return &convoTracker{
-		convos: cache.New(window, window),
-		warns:  cache.New(window, window),
-		window: window,
-	}
-}
-
-// trackConvo records a conversation from sender to recipient at the given time.
-func (w *convoTracker) trackConvo(now time.Time, sender, recip state.IdentScreenName) {
-	k := w.key(sender, recip)
-
-	buf, found := w.convos.Get(k)
-	if !found {
-		buf = &ringBuffer{}
-		w.convos.Set(k, buf, time.Hour)
-	}
-
-	buf.(*ringBuffer).set(now)
-}
-
-// trackWarn attempts to record a warning from warner to warnee.
-// It returns true if the warning is allowed (warnee has sent more messages
-// than warnings in the current window), or false if the warning limit has been
-// reached or no conversation exists in the current window.
-func (w *convoTracker) trackWarn(now time.Time, warner, warnee state.IdentScreenName) bool {
-	key := w.key(warnee, warner)
-
-	convos, found := w.convos.Get(key)
-	if !found {
-		// no convos tracked, can't warn
-		return false
-	}
-
-	windowStart := now.Add(-w.window)
-
-	// get convo count during window
-	var convoCt int
-	for _, v := range convos.(*ringBuffer).vals {
-		if v.After(windowStart) {
-			convoCt++
-		}
-	}
-
-	warns, found := w.warns.Get(key)
-	if !found {
-		warns = &ringBuffer{}
-		w.warns.Set(key, warns, time.Hour)
-	}
-
-	// get warn count during window
-	var warnCount int
-	for _, v := range warns.(*ringBuffer).vals {
-		if v.After(windowStart) {
-			warnCount++
-		}
-	}
-
-	if convoCt <= warnCount {
-		return false
-	}
-
-	warns.(*ringBuffer).set(now)
-
-	return true
-}
-
-func (w *convoTracker) key(sender state.IdentScreenName, recip state.IdentScreenName) string {
-	return sender.String() + recip.String()
-}
-
-// ringBuffer is a fixed-size circular buffer with 3 slots for storing time values.
-type ringBuffer struct {
-	cur  int          // Current cursor position (0, 1, or 2)
-	vals [3]time.Time // Fixed-size array to store time values
-}
-
-// val returns the time at the current cursor position.
-func (r *ringBuffer) val() time.Time {
-	return r.vals[r.cur]
-}
-
-// set stores the given time at the current cursor position and advances the cursor.
-func (r *ringBuffer) set(v time.Time) {
-	r.vals[r.cur] = v
-	r.cur = (r.cur + 1) % 3
-}

+ 2 - 363
foodgroup/icbm_test.go

@@ -2,10 +2,8 @@ package foodgroup
 
 import (
 	"context"
-	"log/slog"
 	"net"
 	"net/netip"
-	"sync"
 	"testing"
 	"time"
 
@@ -802,7 +800,6 @@ func TestICBMService_ChannelMsgToHost(t *testing.T) {
 				offlineMessageSaver: offlineMessageManager,
 				sessionRetriever:    sessionRetriever,
 				timeNow:             tc.timeNow,
-				convoTracker:        newConvoTracker(),
 			}
 
 			outputSNAC, err := svc.ChannelMsgToHost(context.Background(), tc.senderSession, tc.inputSNAC.Frame,
@@ -942,8 +939,6 @@ func TestICBMService_EvilRequest(t *testing.T) {
 		name string
 		// senderScreenName is the session of the user sending the EvilRequest
 		senderSession *state.Session
-		// msgsReceived is the # of messages received from the warned user
-		msgsReceived int
 		// inputSNAC is the SNAC sent by the sender client
 		inputSNAC wire.SNACMessage
 		// expectOutput is the SNAC sent from the server to client
@@ -951,13 +946,10 @@ func TestICBMService_EvilRequest(t *testing.T) {
 		// mockParams is the list of params sent to mocks that satisfy this
 		// method's dependencies
 		mockParams mockParams
-		// waitForWarnMsg indicates whether to wait for session warn signal
-		waitForWarnMsg bool
 	}{
 		{
 			name:          "transmit anonymous warning from sender to recipient",
 			senderSession: newTestSession("sender-screen-name"),
-			msgsReceived:  1,
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
@@ -1026,12 +1018,10 @@ func TestICBMService_EvilRequest(t *testing.T) {
 					},
 				},
 			},
-			waitForWarnMsg: true,
 		},
 		{
 			name:          "transmit non-anonymous warning from sender to recipient",
 			senderSession: newTestSession("sender-screen-name", sessOptWarning(110)),
-			msgsReceived:  1,
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
@@ -1108,12 +1098,10 @@ func TestICBMService_EvilRequest(t *testing.T) {
 					},
 				},
 			},
-			waitForWarnMsg: true,
 		},
 		{
 			name:          "don't transmit non-anonymous warning from sender to recipient because sender has blocked recipient",
 			senderSession: newTestSession("sender-screen-name"),
-			msgsReceived:  1,
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
@@ -1154,7 +1142,6 @@ func TestICBMService_EvilRequest(t *testing.T) {
 		{
 			name:          "don't transmit non-anonymous warning from sender to recipient because recipient has blocked sender",
 			senderSession: newTestSession("sender-screen-name"),
-			msgsReceived:  1,
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
@@ -1195,7 +1182,6 @@ func TestICBMService_EvilRequest(t *testing.T) {
 		{
 			name:          "don't let users warn themselves",
 			senderSession: newTestSession("sender-screen-name"),
-			msgsReceived:  1,
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
@@ -1219,7 +1205,6 @@ func TestICBMService_EvilRequest(t *testing.T) {
 		{
 			name:          "don't transmit non-anonymous warning from sender to recipient because recipient is offline",
 			senderSession: newTestSession("sender-screen-name"),
-			msgsReceived:  1,
 			mockParams: mockParams{
 				relationshipFetcherParams: relationshipFetcherParams{
 					relationshipParams: relationshipParams{
@@ -1268,7 +1253,6 @@ func TestICBMService_EvilRequest(t *testing.T) {
 		{
 			name:          "don't transmit anonymous warning from sender to recipient because recipient is offline",
 			senderSession: newTestSession("sender-screen-name"),
-			msgsReceived:  1,
 			mockParams: mockParams{
 				relationshipFetcherParams: relationshipFetcherParams{
 					relationshipParams: relationshipParams{
@@ -1354,38 +1338,18 @@ func TestICBMService_EvilRequest(t *testing.T) {
 				messageRelayer:      messageRelayer,
 				offlineMessageSaver: offlineMessageManager,
 				sessionRetriever:    sessionRetriever,
-				convoTracker:        newConvoTracker(),
-				snacRateLimits:      wire.DefaultSNACRateLimits(),
 			}
 
-			for i := 0; i < tc.msgsReceived; i++ {
-				svc.convoTracker.trackConvo(time.Now(),
-					state.NewIdentScreenName(tc.inputSNAC.Body.(wire.SNAC_0x04_0x08_ICBMEvilRequest).ScreenName),
-					tc.senderSession.IdentScreenName())
-			}
-
-			var wg sync.WaitGroup
-			if tc.waitForWarnMsg {
-				wg.Add(1)
-				go func() {
-					defer wg.Done()
-					for _, sess := range tc.mockParams.sessionRetrieverParams.retrieveSessionParams {
-						<-sess.result.WarningCh()
-					}
-				}()
-			}
 			outputSNAC, err := svc.EvilRequest(context.Background(), tc.senderSession, tc.inputSNAC.Frame,
 				tc.inputSNAC.Body.(wire.SNAC_0x04_0x08_ICBMEvilRequest))
 			assert.NoError(t, err)
 			assert.Equal(t, tc.expectOutput, outputSNAC)
-
-			wg.Wait()
 		})
 	}
 }
 
 func TestICBMService_ParameterQuery(t *testing.T) {
-	svc := NewICBMService(nil, nil, nil, nil, nil, wire.DefaultSNACRateLimits(), slog.Default())
+	svc := NewICBMService(nil, nil, nil, nil, nil, wire.DefaultSNACRateLimits())
 
 	have := svc.ParameterQuery(nil, wire.SNACFrame{RequestID: 1234})
 	want := wire.SNACMessage{
@@ -1437,333 +1401,8 @@ func TestICBMService_ClientErr(t *testing.T) {
 	messageRelayer.EXPECT().
 		RelayToScreenName(mock.Anything, state.NewIdentScreenName("recipientScreenName"), expect)
 
-	svc := NewICBMService(nil, messageRelayer, nil, nil, nil, wire.DefaultSNACRateLimits(), slog.Default())
+	svc := NewICBMService(nil, messageRelayer, nil, nil, nil, wire.DefaultSNACRateLimits())
 
 	err := svc.ClientErr(context.Background(), sess, wire.SNACFrame{RequestID: 1234}, inBody)
 	assert.NoError(t, err)
 }
-
-func TestRingBuffer(t *testing.T) {
-	t.Run("new ringBuffer should have zero values", func(t *testing.T) {
-		rb := &ringBuffer{}
-
-		// Test val() on empty ringBuffer - should return zero time
-		result := rb.val()
-		zeroTime := time.Time{}
-		assert.Equal(t, zeroTime, result)
-	})
-
-	t.Run("val() should return current time", func(t *testing.T) {
-		now := time.Now()
-		rb := &ringBuffer{
-			cur: 1,
-			vals: [3]time.Time{
-				now.Add(-2 * time.Hour),
-				now.Add(-1 * time.Hour),
-				now,
-			},
-		}
-
-		result := rb.val()
-		assert.Equal(t, rb.vals[1], result)
-	})
-
-	t.Run("set() should store time and advance cursor", func(t *testing.T) {
-		rb := &ringBuffer{cur: 0}
-		newTime := time.Now()
-
-		// Set the time
-		rb.set(newTime)
-
-		// After set, cursor should advance to position 1
-		// We can verify this by setting another time and checking that it's stored at position 1
-		secondTime := time.Now().Add(time.Hour)
-		rb.set(secondTime)
-
-		// Now cursor should be at position 2, so val() should return the time at position 2
-		// But since we only set 2 times, position 2 should still be the zero time
-		zeroTime := time.Time{}
-		assert.Equal(t, zeroTime, rb.val())
-	})
-
-	t.Run("set() should wrap around after reaching end of array", func(t *testing.T) {
-		rb := &ringBuffer{cur: 2}
-		newTime := time.Now()
-
-		// Set the time at position 2
-		rb.set(newTime)
-
-		// Cursor should wrap around to position 0
-		// We can verify this by setting another time and checking behavior
-		rb.set(time.Now().Add(time.Hour))
-
-		// Now cursor should be at position 1, so val() should return the time at position 1
-		// But since we only set 2 times, position 1 should still be the zero time
-		zeroTime := time.Time{}
-		assert.Equal(t, zeroTime, rb.val())
-	})
-
-	t.Run("set() should handle multiple insertions correctly", func(t *testing.T) {
-		rb := &ringBuffer{}
-
-		// Insert 3 times
-		time1 := time.Now()
-		time2 := time.Now().Add(time.Hour)
-		time3 := time.Now().Add(2 * time.Hour)
-
-		rb.set(time1)
-		rb.set(time2)
-		rb.set(time3)
-
-		// After 3 insertions, cursor should be at position 0
-		// So val() should return the time at position 0
-		// This should be time1 since it was the first time set
-		assert.Equal(t, time1, rb.val())
-	})
-
-	t.Run("set() should overwrite existing values in order", func(t *testing.T) {
-		rb := &ringBuffer{}
-
-		// Set 3 times to fill the buffer
-		rb.set(time.Now())
-		rb.set(time.Now().Add(time.Hour))
-		rb.set(time.Now().Add(2 * time.Hour))
-
-		// After 3 sets, cursor is at position 0, val() returns first time
-		firstTime := rb.val()
-		assert.False(t, firstTime.IsZero())
-
-		// Set a 4th time - should overwrite position 0
-		fourthTime := time.Now().Add(3 * time.Hour)
-		rb.set(fourthTime)
-
-		// Now cursor is at position 1, val() returns second time
-		secondTime := rb.val()
-		assert.False(t, secondTime.IsZero())
-
-		// Set a 5th time - should overwrite position 1
-		fifthTime := time.Now().Add(4 * time.Hour)
-		rb.set(fifthTime)
-
-		// Now cursor is at position 2, val() returns third time
-		thirdTime := rb.val()
-		assert.False(t, thirdTime.IsZero())
-
-		// Set a 6th time - should overwrite position 2
-		sixthTime := time.Now().Add(5 * time.Hour)
-		rb.set(sixthTime)
-
-		// Now cursor wraps around to position 0, val() returns fourth time
-		assert.Equal(t, fourthTime, rb.val())
-	})
-
-	t.Run("val() should return correct time after multiple operations", func(t *testing.T) {
-		rb := &ringBuffer{}
-
-		// Insert times and verify val() returns correct current time
-		time1 := time.Now()
-		time2 := time.Now().Add(time.Hour)
-
-		rb.set(time1)
-		rb.set(time2)
-
-		// After 2 sets, cursor is at position 2
-		// So val() should return the time at position 2
-		// But since we only set 2 times, position 2 should still be the zero time
-		zeroTime := time.Time{}
-		assert.Equal(t, zeroTime, rb.val())
-
-		// Set one more to wrap around
-		time3 := time.Now().Add(2 * time.Hour)
-		rb.set(time3)
-
-		// Now cursor is at position 0, so val() should return the time at position 0
-		// This should be time1
-		assert.Equal(t, time1, rb.val())
-	})
-
-	t.Run("ringBuffer should maintain circular behavior over many operations", func(t *testing.T) {
-		rb := &ringBuffer{}
-
-		// Perform many operations to test circular behavior
-		for i := 0; i < 10; i++ {
-			rb.set(time.Now().Add(time.Duration(i) * time.Hour))
-		}
-
-		// After 10 operations, cursor should be at position 1 (10 % 3 = 1)
-		// So val() should return the time at position 1
-		// This should be the 8th time set (at position 1)
-		// We can't compare exact times since they're set in a loop, so just verify it's not zero
-		assert.False(t, rb.val().IsZero())
-
-		// Set one more to advance cursor to position 2
-		rb.set(time.Now().Add(10 * time.Hour))
-
-		// Now cursor is at position 2, so val() should return the time at position 2
-		// This should be the 9th time set (at position 2)
-		assert.False(t, rb.val().IsZero())
-
-		// Set one more to wrap around to position 0
-		rb.set(time.Now().Add(11 * time.Hour))
-
-		// Now cursor is at position 0, so val() should return the time at position 0
-		// This should be the 10th time set (at position 0)
-		assert.False(t, rb.val().IsZero())
-	})
-
-	t.Run("ringBuffer should cycle through all positions correctly", func(t *testing.T) {
-		rb := &ringBuffer{}
-
-		// Test cycling through all 3 positions
-		times := []time.Time{
-			time.Now(),
-			time.Now().Add(time.Hour),
-			time.Now().Add(2 * time.Hour),
-		}
-
-		// Set all 3 times
-		for _, t := range times {
-			rb.set(t)
-		}
-
-		// After 3 sets, cursor should be at position 0
-		// So val() should return the time at position 0
-		assert.Equal(t, times[0], rb.val())
-
-		// Set one more to advance cursor to position 1
-		nextTime := time.Now().Add(3 * time.Hour)
-		rb.set(nextTime)
-
-		// Now cursor is at position 1, so val() should return the time at position 1
-		// This should be the second time since it was stored at position 1
-		assert.Equal(t, times[1], rb.val())
-	})
-}
-
-func TestConvoTracker(t *testing.T) {
-	ct := newConvoTracker()
-	sender := state.NewIdentScreenName("sender")
-	recip := state.NewIdentScreenName("recipient")
-	now := time.Now()
-
-	// can't warn until a message is sent
-	assert.False(t, ct.trackWarn(now, recip, sender))
-
-	// can warn 1st time
-	ct.trackConvo(now, sender, recip)
-	assert.True(t, ct.trackWarn(now, recip, sender))
-
-	// can't warn 2nd time until 2nd message is sent
-	assert.False(t, ct.trackWarn(now, recip, sender))
-
-	// can warn 2nd time
-	now = now.Add(1 * time.Second)
-	ct.trackConvo(now, sender, recip)
-	assert.True(t, ct.trackWarn(now, recip, sender))
-
-	// can't warn 3rd time until 3rd message is sent
-	assert.False(t, ct.trackWarn(now, recip, sender))
-
-	// can warn 3rd time
-	now = now.Add(1 * time.Second)
-	ct.trackConvo(now, sender, recip)
-	assert.True(t, ct.trackWarn(now, recip, sender))
-
-	// can't warn 4th time
-	now = now.Add(1 * time.Second)
-	ct.trackConvo(now, sender, recip)
-	assert.False(t, ct.trackWarn(now, recip, sender))
-
-	// let an hour pass, we should be able to warn again
-	now = now.Add(1 * time.Hour)
-	ct.trackConvo(now, sender, recip)
-	assert.True(t, ct.trackWarn(now, recip, sender))
-}
-
-func TestICBMService_DecayWarnLevel(t *testing.T) {
-
-	t.Run("happy path", func(t *testing.T) {
-
-		sess := newTestSession("screen-name")
-		warnCh := make(chan uint16)
-
-		mockBuddyBroadcaster := newMockbuddyBroadcaster(t)
-		mockBuddyBroadcaster.EXPECT().
-			BroadcastBuddyArrived(mock.Anything, matchSession(sess.IdentScreenName())).
-			Run(func(ctx context.Context, sess *state.Session) {
-				warnCh <- sess.Warning()
-			}).Return(nil)
-
-		svc := ICBMService{
-			buddyBroadcaster: mockBuddyBroadcaster,
-			logger:           slog.Default(),
-			interval:         1 * time.Millisecond,
-			snacRateLimits:   wire.DefaultSNACRateLimits(),
-		}
-
-		ctx, cancel := context.WithCancel(context.Background())
-		defer cancel()
-
-		var wg sync.WaitGroup
-		wg.Add(1)
-		go func() {
-			defer wg.Done()
-			svc.DecayWarnLevel(ctx, sess)
-		}()
-
-		sess.IncrementWarning(100, 3)
-		sess.NotifyWarning(ctx)
-
-		assert.Equal(t, uint16(50), <-warnCh)
-		assert.Equal(t, uint16(0), <-warnCh)
-
-		sess.IncrementWarning(50, 3)
-		sess.NotifyWarning(ctx)
-		sess.IncrementWarning(50, 3)
-		sess.NotifyWarning(ctx)
-
-		assert.Equal(t, uint16(50), <-warnCh)
-		assert.Equal(t, uint16(0), <-warnCh)
-
-		cancel()
-		wg.Wait()
-	})
-
-	t.Run("3% burn down clamps to 0", func(t *testing.T) {
-
-		sess := newTestSession("screen-name")
-		warnCh := make(chan uint16)
-
-		mockBuddyBroadcaster := newMockbuddyBroadcaster(t)
-		mockBuddyBroadcaster.EXPECT().
-			BroadcastBuddyArrived(mock.Anything, matchSession(sess.IdentScreenName())).
-			Run(func(ctx context.Context, sess *state.Session) {
-				warnCh <- sess.Warning()
-			}).Return(nil)
-
-		svc := ICBMService{
-			buddyBroadcaster: mockBuddyBroadcaster,
-			logger:           slog.Default(),
-			interval:         1 * time.Millisecond,
-			snacRateLimits:   wire.DefaultSNACRateLimits(),
-		}
-
-		ctx, cancel := context.WithCancel(context.Background())
-		defer cancel()
-
-		var wg sync.WaitGroup
-		wg.Add(1)
-		go func() {
-			defer wg.Done()
-			svc.DecayWarnLevel(ctx, sess)
-		}()
-
-		sess.IncrementWarning(30, 3)
-		sess.NotifyWarning(ctx)
-
-		assert.Equal(t, uint16(0), <-warnCh)
-
-		cancel()
-		wg.Wait()
-	})
-}

+ 0 - 47
server/oscar/mock_departure_notifier_test.go

@@ -22,53 +22,6 @@ func (_m *mockDepartureNotifier) EXPECT() *mockDepartureNotifier_Expecter {
 	return &mockDepartureNotifier_Expecter{mock: &_m.Mock}
 }
 
-// BroadcastBuddyArrived provides a mock function with given fields: ctx, sess
-func (_m *mockDepartureNotifier) BroadcastBuddyArrived(ctx context.Context, sess *state.Session) error {
-	ret := _m.Called(ctx, sess)
-
-	if len(ret) == 0 {
-		panic("no return value specified for BroadcastBuddyArrived")
-	}
-
-	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, *state.Session) error); ok {
-		r0 = rf(ctx, sess)
-	} else {
-		r0 = ret.Error(0)
-	}
-
-	return r0
-}
-
-// mockDepartureNotifier_BroadcastBuddyArrived_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BroadcastBuddyArrived'
-type mockDepartureNotifier_BroadcastBuddyArrived_Call struct {
-	*mock.Call
-}
-
-// BroadcastBuddyArrived is a helper method to define mock.On call
-//   - ctx context.Context
-//   - sess *state.Session
-func (_e *mockDepartureNotifier_Expecter) BroadcastBuddyArrived(ctx interface{}, sess interface{}) *mockDepartureNotifier_BroadcastBuddyArrived_Call {
-	return &mockDepartureNotifier_BroadcastBuddyArrived_Call{Call: _e.mock.On("BroadcastBuddyArrived", ctx, sess)}
-}
-
-func (_c *mockDepartureNotifier_BroadcastBuddyArrived_Call) Run(run func(ctx context.Context, sess *state.Session)) *mockDepartureNotifier_BroadcastBuddyArrived_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*state.Session))
-	})
-	return _c
-}
-
-func (_c *mockDepartureNotifier_BroadcastBuddyArrived_Call) Return(_a0 error) *mockDepartureNotifier_BroadcastBuddyArrived_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *mockDepartureNotifier_BroadcastBuddyArrived_Call) RunAndReturn(run func(context.Context, *state.Session) error) *mockDepartureNotifier_BroadcastBuddyArrived_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // BroadcastBuddyDeparted provides a mock function with given fields: ctx, sess
 func (_m *mockDepartureNotifier) BroadcastBuddyDeparted(ctx context.Context, sess *state.Session) error {
 	ret := _m.Called(ctx, sess)

+ 0 - 34
server/oscar/mock_icbm_service_test.go

@@ -183,40 +183,6 @@ func (_c *mockICBMService_ClientEvent_Call) RunAndReturn(run func(context.Contex
 	return _c
 }
 
-// DecayWarnLevel provides a mock function with given fields: ctx, sess
-func (_m *mockICBMService) DecayWarnLevel(ctx context.Context, sess *state.Session) {
-	_m.Called(ctx, sess)
-}
-
-// mockICBMService_DecayWarnLevel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DecayWarnLevel'
-type mockICBMService_DecayWarnLevel_Call struct {
-	*mock.Call
-}
-
-// DecayWarnLevel is a helper method to define mock.On call
-//   - ctx context.Context
-//   - sess *state.Session
-func (_e *mockICBMService_Expecter) DecayWarnLevel(ctx interface{}, sess interface{}) *mockICBMService_DecayWarnLevel_Call {
-	return &mockICBMService_DecayWarnLevel_Call{Call: _e.mock.On("DecayWarnLevel", ctx, sess)}
-}
-
-func (_c *mockICBMService_DecayWarnLevel_Call) Run(run func(ctx context.Context, sess *state.Session)) *mockICBMService_DecayWarnLevel_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*state.Session))
-	})
-	return _c
-}
-
-func (_c *mockICBMService_DecayWarnLevel_Call) Return() *mockICBMService_DecayWarnLevel_Call {
-	_c.Call.Return()
-	return _c
-}
-
-func (_c *mockICBMService_DecayWarnLevel_Call) RunAndReturn(run func(context.Context, *state.Session)) *mockICBMService_DecayWarnLevel_Call {
-	_c.Run(run)
-	return _c
-}
-
 // EvilRequest provides a mock function with given fields: ctx, sess, inFrame, inBody
 func (_m *mockICBMService) EvilRequest(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x08_ICBMEvilRequest) (wire.SNACMessage, error) {
 	ret := _m.Called(ctx, sess, inFrame, inBody)

+ 0 - 4
server/oscar/server.go

@@ -34,7 +34,6 @@ func NewServer(
 	limits wire.SNACRateLimits,
 	limiter *IPRateLimiter,
 	listenerCfg []config.Listener,
-	lowerWarnLevel func(ctx context.Context, sess *state.Session),
 ) *Server {
 	oscarSvc := oscarServer{
 		AuthService:        authService,
@@ -47,7 +46,6 @@ func NewServer(
 		RateLimitUpdater:   rateLimitUpdater,
 		SNACRateLimits:     limits,
 		IPRateLimiter:      limiter,
-		lowerWarnLevel:     lowerWarnLevel,
 	}
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -190,7 +188,6 @@ type oscarServer struct {
 	RateLimitUpdater
 	wire.SNACRateLimits
 	*IPRateLimiter
-	lowerWarnLevel func(ctx context.Context, sess *state.Session)
 }
 
 func (s oscarServer) routeConnection(ctx context.Context, conn net.Conn, listener config.Listener) error {
@@ -276,7 +273,6 @@ func (s oscarServer) connectToOSCARService(
 			sess.SetRemoteAddr(&ip)
 		}
 
-		go s.lowerWarnLevel(ctx, sess)
 		go s.receiveSessMessages(ctx, sess, flapc)
 
 	case wire.Chat:

+ 4 - 10
server/oscar/server_test.go

@@ -12,13 +12,12 @@ import (
 	"testing"
 	"time"
 
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-	"golang.org/x/time/rate"
-
 	"github.com/mk6i/retro-aim-server/config"
 	"github.com/mk6i/retro-aim-server/state"
 	"github.com/mk6i/retro-aim-server/wire"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"golang.org/x/time/rate"
 )
 
 func TestServer_ListenAndServeAndShutdown(t *testing.T) {
@@ -55,7 +54,6 @@ func TestServer_ListenAndServeAndShutdown(t *testing.T) {
 		wire.DefaultSNACRateLimits(),
 		nil,
 		cfg,
-		func(ctx context.Context, sess *state.Session) {},
 	)
 
 	server.handler = func(ctx context.Context, conn net.Conn, listener config.Listener) error {
@@ -405,7 +403,7 @@ func TestOscarServer_RouteConnection_BOS(t *testing.T) {
 	chatSessionManager.EXPECT().
 		RemoveUserFromAllChats(mock.Anything)
 
-	wg.Add(2)
+	wg.Add(1)
 	handler := func(ctx context.Context, serverType uint16, sess *state.Session, inFrame wire.SNACFrame, r io.Reader, rw ResponseWriter, listener config.Listener) error {
 		defer wg.Done()
 		return nil
@@ -419,9 +417,6 @@ func TestOscarServer_RouteConnection_BOS(t *testing.T) {
 		BuddyListRegistry:  buddyListRegistry,
 		ChatSessionManager: chatSessionManager,
 		DepartureNotifier:  departureNotifier,
-		lowerWarnLevel: func(ctx context.Context, sess *state.Session) {
-			defer wg.Done()
-		},
 	}
 	assert.NoError(t, rt.routeConnection(context.Background(), clientFake, config.Listener{}))
 
@@ -760,7 +755,6 @@ func Test_oscarServer_receiveSessMessages_BOS_integration(t *testing.T) {
 		DepartureNotifier:  departureNotifier,
 		OnlineNotifier:     onlineNotifier,
 		Logger:             slog.New(slog.NewTextHandler(io.Discard, nil)),
-		lowerWarnLevel:     func(ctx context.Context, sess *state.Session) {},
 	}
 
 	// Fake client connection with address

+ 0 - 2
server/oscar/types.go

@@ -30,7 +30,6 @@ type BuddyListRegistry interface {
 // DepartureNotifier is the interface for sending buddy departure notifications
 // when a client disconnects.
 type DepartureNotifier interface {
-	BroadcastBuddyArrived(ctx context.Context, sess *state.Session) error
 	BroadcastBuddyDeparted(ctx context.Context, sess *state.Session) error
 }
 
@@ -103,7 +102,6 @@ type ICBMService interface {
 	EvilRequest(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x08_ICBMEvilRequest) (wire.SNACMessage, error)
 	ParameterQuery(ctx context.Context, inFrame wire.SNACFrame) wire.SNACMessage
 	ClientErr(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x04_0x0B_ICBMClientErr) error
-	DecayWarnLevel(ctx context.Context, sess *state.Session)
 }
 
 type ICQService interface {

+ 1 - 16
server/toc/server.go

@@ -122,14 +122,7 @@ func (l *IPRateLimiter) Allow(ip string) (allowed bool) {
 	return limiter.(*rate.Limiter).Allow()
 }
 
-func NewServer(
-	listenerCfg []string,
-	logger *slog.Logger,
-	BOSProxy OSCARProxy,
-	ipRateLimiter *IPRateLimiter,
-	lowerWarnLevel func(ctx context.Context, sess *state.Session),
-) *Server {
-
+func NewServer(listenerCfg []string, logger *slog.Logger, BOSProxy OSCARProxy, ipRateLimiter *IPRateLimiter) *Server {
 	ctx, cancel := context.WithCancel(context.Background())
 
 	s := &Server{
@@ -138,7 +131,6 @@ func NewServer(
 		listenerCfg:        listenerCfg,
 		logger:             logger,
 		loginIPRateLimiter: ipRateLimiter,
-		lowerWarnLevel:     lowerWarnLevel,
 		servers:            make([]*http.Server, 0, len(listenerCfg)),
 		shutdownCancel:     cancel,
 		shutdownCtx:        ctx,
@@ -163,7 +155,6 @@ type Server struct {
 	bosProxy           OSCARProxy
 	logger             *slog.Logger
 	loginIPRateLimiter *IPRateLimiter
-	lowerWarnLevel     func(ctx context.Context, sess *state.Session)
 
 	listenerCfg []string
 	listeners   []net.Listener
@@ -421,12 +412,6 @@ func (s *Server) handleTOCRequest(
 		return errors.Join(err, errServerWrite)
 	})
 
-	// process warning limits
-	g.Go(func() error {
-		s.lowerWarnLevel(ctx, sessBOS)
-		return nil
-	})
-
 	return g.Wait()
 }
 

+ 10 - 16
server/toc/server_test.go

@@ -8,7 +8,6 @@ import (
 	"sync"
 	"testing"
 
-	"github.com/mk6i/retro-aim-server/state"
 	"github.com/mk6i/retro-aim-server/wire"
 
 	"github.com/stretchr/testify/assert"
@@ -24,9 +23,8 @@ func TestServer_handleTOCRequest_serverShutdown(t *testing.T) {
 	go func() {
 		defer wg.Done()
 		sv := Server{
-			bosProxy:       testOSCARProxy(t),
-			logger:         slog.Default(),
-			lowerWarnLevel: func(ctx context.Context, sess *state.Session) {},
+			bosProxy: testOSCARProxy(t),
+			logger:   slog.Default(),
 		}
 
 		serverReader, _ := io.Pipe()
@@ -63,9 +61,8 @@ func TestServer_handleTOCRequest_clientReadDisconnect(t *testing.T) {
 		fc := wire.NewFlapClient(0, serverReader, nil)
 
 		sv := Server{
-			bosProxy:       testOSCARProxy(t),
-			logger:         slog.Default(),
-			lowerWarnLevel: func(ctx context.Context, sess *state.Session) {},
+			bosProxy: testOSCARProxy(t),
+			logger:   slog.Default(),
 		}
 		err := sv.handleTOCRequest(context.Background(), closeConn, sess, NewChatRegistry(), fc)
 		assert.ErrorIs(t, err, errClientReq)
@@ -97,9 +94,8 @@ func TestServer_handleTOCRequest_sessClose(t *testing.T) {
 		}
 
 		sv := Server{
-			bosProxy:       testOSCARProxy(t),
-			logger:         slog.Default(),
-			lowerWarnLevel: func(ctx context.Context, sess *state.Session) {},
+			bosProxy: testOSCARProxy(t),
+			logger:   slog.Default(),
 		}
 		err := sv.handleTOCRequest(context.Background(), closeConn, sess, NewChatRegistry(), fc)
 		assert.ErrorIs(t, err, errTOCProcessing)
@@ -130,9 +126,8 @@ func TestServer_handleTOCRequest_replyFailure(t *testing.T) {
 		fc := wire.NewFlapClient(0, serverReader, serverWriter)
 
 		sv := Server{
-			bosProxy:       testOSCARProxy(t),
-			logger:         slog.Default(),
-			lowerWarnLevel: func(ctx context.Context, sess *state.Session) {},
+			bosProxy: testOSCARProxy(t),
+			logger:   slog.Default(),
 		}
 		err := sv.handleTOCRequest(context.Background(), closeConn, sess, NewChatRegistry(), fc)
 		assert.ErrorIs(t, err, errServerWrite)
@@ -168,9 +163,8 @@ func TestServer_handleTOCRequest_happyPath(t *testing.T) {
 		}
 		fc := wire.NewFlapClient(0, serverReader, serverWriter)
 		sv := Server{
-			bosProxy:       testOSCARProxy(t),
-			logger:         slog.Default(),
-			lowerWarnLevel: func(ctx context.Context, sess *state.Session) {},
+			bosProxy: testOSCARProxy(t),
+			logger:   slog.Default(),
 		}
 		err := sv.handleTOCRequest(context.Background(), closeConn, newTestSession("me"), NewChatRegistry(), fc)
 		assert.ErrorIs(t, err, errClientReq)

+ 38 - 100
state/session.go

@@ -1,7 +1,6 @@
 package state
 
 import (
-	"context"
 	"net/netip"
 	"sync"
 	"time"
@@ -50,33 +49,31 @@ const (
 // Session represents a user's current session. Unless stated otherwise, all
 // methods may be safely accessed by multiple goroutines.
 type Session struct {
-	awayMessage           string
-	caps                  [][16]byte
-	chatRoomCookie        string
-	clientID              string
-	closed                bool
-	displayScreenName     DisplayScreenName
-	foodGroupVersions     [wire.MDir + 1]uint16
-	identScreenName       IdentScreenName
-	idle                  bool
-	idleTime              time.Time
-	lastObservedStates    [5]RateClassState
-	msgCh                 chan wire.SNACMessage
-	multiConnFlag         wire.MultiConnFlag
-	mutex                 sync.RWMutex
-	nowFn                 func() time.Time
-	rateByClassID         [5]RateClassState
-	rateByClassIDOriginal [5]RateClassState
-	remoteAddr            *netip.AddrPort
-	signonComplete        bool
-	signonTime            time.Time
-	stopCh                chan struct{}
-	typingEventsEnabled   bool
-	uin                   uint32
-	userInfoBitmask       uint16
-	userStatusBitmask     uint32
-	warning               uint16
-	warningCh             chan struct{}
+	awayMessage         string
+	caps                [][16]byte
+	chatRoomCookie      string
+	closed              bool
+	displayScreenName   DisplayScreenName
+	identScreenName     IdentScreenName
+	idle                bool
+	idleTime            time.Time
+	msgCh               chan wire.SNACMessage
+	mutex               sync.RWMutex
+	nowFn               func() time.Time
+	signonComplete      bool
+	signonTime          time.Time
+	stopCh              chan struct{}
+	uin                 uint32
+	warning             uint16
+	userInfoBitmask     uint16
+	userStatusBitmask   uint32
+	clientID            string
+	remoteAddr          *netip.AddrPort
+	lastObservedStates  [5]RateClassState
+	rateByClassID       [5]RateClassState
+	foodGroupVersions   [wire.MDir + 1]uint16
+	typingEventsEnabled bool
+	multiConnFlag       wire.MultiConnFlag
 }
 
 // NewSession returns a new instance of Session. By default, the user may have
@@ -119,7 +116,6 @@ func NewSession() *Session {
 			vals[wire.MDir] = 1
 			return vals
 		}(),
-		warningCh: make(chan struct{}),
 	}
 }
 
@@ -145,7 +141,6 @@ func (s *Session) SetRateClasses(now time.Time, classes wire.RateLimitClasses) {
 	}
 
 	s.rateByClassID = newStates
-	s.rateByClassIDOriginal = newStates
 }
 
 // SetRemoteAddr sets the user's remote IP address
@@ -192,77 +187,12 @@ func (s *Session) SetUserStatusBitmask(bitmask uint32) {
 	s.userStatusBitmask = bitmask
 }
 
-// IncrementWarning increments the user's warning level and scales rate limits accordingly.
-// The incr parameter is the warning increment (negative to decrease), and classID specifies
-// which rate limit class to scale. The incr param is a percentage represented as an integer
-// where 30 = 3.0%, 100 = 10.0%, etc.
-func (s *Session) IncrementWarning(incr int16, classID wire.RateLimitClassID) bool {
+// IncrementWarning increments the user's warning level. To decrease, pass a
+// negative increment value.
+func (s *Session) IncrementWarning(incr uint16) {
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
-
-	// Handle warning level increment
-	newWarning := int32(s.warning) + int32(incr)
-	if newWarning > 1000 {
-		return false
-	}
-	if newWarning < 0 {
-		s.warning = 0 // clamp min at 0
-	} else {
-		s.warning = uint16(newWarning)
-	}
-
-	pct := float32(incr) / 1000.0
-
-	// create reference variables for better readability
-	rateClass := &s.rateByClassID[classID-1]
-	originalRateClass := &s.rateByClassIDOriginal[classID-1]
-
-	// clamp function to constrain values between min and max
-	clamp := func(value, min, max int32) int32 {
-		if value < min {
-			return min
-		}
-		if value > max {
-			return max
-		}
-		return value
-	}
-
-	// scale the rate limit parameters
-	newLimitLevel := rateClass.LimitLevel + int32(float32(originalRateClass.MaxLevel-originalRateClass.LimitLevel)*pct)
-	rateClass.LimitLevel = clamp(newLimitLevel, originalRateClass.LimitLevel, originalRateClass.MaxLevel)
-
-	newLimitLevel = rateClass.ClearLevel + int32(float32(originalRateClass.MaxLevel-originalRateClass.ClearLevel)*pct)
-	rateClass.ClearLevel = clamp(newLimitLevel, originalRateClass.ClearLevel, originalRateClass.MaxLevel)
-
-	newLimitLevel = rateClass.AlertLevel + int32(float32(originalRateClass.MaxLevel-originalRateClass.AlertLevel)*pct)
-	rateClass.AlertLevel = clamp(newLimitLevel, originalRateClass.AlertLevel, originalRateClass.MaxLevel)
-
-	return true
-}
-
-// Warning returns the user's current warning level as a percentage.
-// The warning level is stored as an integer representation of a percentage
-// where 30 = 3.0%, 100 = 10.0%, 1000 = 100.0%, etc.
-// This is how the OSCAR protocol represents warning percentages.
-func (s *Session) Warning() uint16 {
-	s.mutex.RLock()
-	defer s.mutex.RUnlock()
-	return s.warning
-}
-
-// NotifyWarning sends a warning notification signal to any listeners.
-func (s *Session) NotifyWarning(ctx context.Context) {
-	select {
-	case s.warningCh <- struct{}{}:
-	case <-ctx.Done():
-	}
-}
-
-// WarningCh returns the warning notification channel.
-// Listeners can receive from this channel to be notified when warnings occur.
-func (s *Session) WarningCh() chan struct{} {
-	return s.warningCh
+	s.warning += incr
 }
 
 // Invisible returns true if the user is idle.
@@ -407,7 +337,7 @@ func (s *Session) TLVUserInfo() wire.TLVUserInfo {
 	defer s.mutex.RUnlock()
 	return wire.TLVUserInfo{
 		ScreenName:   string(s.displayScreenName),
-		WarningLevel: uint16(s.warning),
+		WarningLevel: s.warning,
 		TLVBlock: wire.TLVBlock{
 			TLVList: s.userInfo(),
 		},
@@ -466,6 +396,14 @@ func (s *Session) Caps() [][16]byte {
 	return s.caps
 }
 
+func (s *Session) Warning() uint16 {
+	s.mutex.RLock()
+	defer s.mutex.RUnlock()
+	var w uint16
+	w = s.warning
+	return w
+}
+
 // ReceiveMessage returns a channel of messages relayed via this session. It
 // may only be read by one consumer. The channel never closes; call this method
 // in a select block along with Closed in order to detect session closure.

+ 3 - 175
state/session_test.go

@@ -24,8 +24,8 @@ func TestSession_SetAndGetAwayMessage(t *testing.T) {
 func TestSession_IncrementAndGetWarning(t *testing.T) {
 	s := NewSession()
 	assert.Zero(t, s.Warning())
-	s.IncrementWarning(1, 1)
-	s.IncrementWarning(2, 1)
+	s.IncrementWarning(1)
+	s.IncrementWarning(2)
 	assert.Equal(t, uint16(3), s.Warning())
 }
 
@@ -89,7 +89,7 @@ func TestSession_TLVUserInfo(t *testing.T) {
 				s.SetSignonTime(time.Unix(1, 0))
 				s.SetIdentScreenName(NewIdentScreenName("xXAIMUSERXx"))
 				s.SetDisplayScreenName("xXAIMUSERXx")
-				s.IncrementWarning(10, 1)
+				s.IncrementWarning(10)
 				s.SetUserInfoFlag(wire.OServiceUserFlagOSCARFree)
 				return s
 			},
@@ -622,175 +622,3 @@ func TestSession_SetAndGetMultiConnFlag(t *testing.T) {
 	s.SetMultiConnFlag(wire.MultiConnFlagsSingleClient)
 	assert.Equal(t, wire.MultiConnFlagsSingleClient, s.MultiConnFlag())
 }
-
-func TestSession_IncrementWarningWithRateLimitScaling(t *testing.T) {
-	t.Run("scale up", func(t *testing.T) {
-		classParams := [5]wire.RateClass{
-			{},
-			{},
-			{
-				ID:              3,
-				WindowSize:      20,
-				ClearLevel:      5100,
-				AlertLevel:      5000,
-				LimitLevel:      4000,
-				DisconnectLevel: 3000,
-				MaxLevel:        6000,
-			},
-			{},
-			{},
-		}
-		rateClasses := wire.NewRateLimitClasses(classParams)
-
-		now := time.Now()
-
-		sess := NewSession()
-		sess.SetRateClasses(now, rateClasses)
-
-		assert.Equal(t, int32(5000), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5100), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4000), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(5100), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5190), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4200), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(5200), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5280), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4400), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(5300), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5370), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4600), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(5400), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5460), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4800), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(5500), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5550), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5000), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(5600), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5640), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5200), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(5700), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5730), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5400), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(5800), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5820), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5600), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(5900), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5910), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5800), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(6000), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(6000), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(6000), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(100, 3)
-		assert.Equal(t, int32(6000), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(6000), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(6000), sess.rateByClassID[2].LimitLevel)
-	})
-
-	t.Run("scale down", func(t *testing.T) {
-		currentClassParams := [5]wire.RateClass{
-			{},
-			{},
-			{
-				ID:              3,
-				WindowSize:      20,
-				ClearLevel:      5100,
-				AlertLevel:      5000,
-				LimitLevel:      4000,
-				DisconnectLevel: 3000,
-				MaxLevel:        6000,
-			},
-			{},
-			{},
-		}
-		rateClasses := wire.NewRateLimitClasses(currentClassParams)
-
-		now := time.Now()
-
-		sess := NewSession()
-		sess.SetRateClasses(now, rateClasses)
-
-		for i := 0; i < 10; i++ {
-			sess.IncrementWarning(100, 3)
-		}
-
-		assert.Equal(t, int32(6000), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(6000), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(6000), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5900), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5910), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5800), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5800), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5820), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5600), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5700), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5730), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5400), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5600), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5640), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5200), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5500), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5550), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(5000), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5400), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5460), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4800), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5300), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5370), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4600), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5200), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5280), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4400), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5100), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5190), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4200), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5000), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5100), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4000), sess.rateByClassID[2].LimitLevel)
-
-		sess.IncrementWarning(-100, 3)
-		assert.Equal(t, int32(5000), sess.rateByClassID[2].AlertLevel)
-		assert.Equal(t, int32(5100), sess.rateByClassID[2].ClearLevel)
-		assert.Equal(t, int32(4000), sess.rateByClassID[2].LimitLevel)
-	})
-}