Mike 1 неделя назад
Родитель
Сommit
ffd13d76dc

+ 5 - 5
server/webapi/aim_handler.go

@@ -106,7 +106,7 @@ type StartSessionEvents struct {
 	MyInfo     *MyInfo         `json:"myInfo,omitempty" xml:"myInfo,omitempty"`
 	BuddyList  *BuddyListData  `json:"buddylist,omitempty" xml:"buddylist,omitempty"`
 	Preference *PreferenceData `json:"preference,omitempty" xml:"preference,omitempty"`
-	PermitDeny interface{}     `json:"permitDeny,omitempty" xml:"permitDeny,omitempty"`
+	PermitDeny any             `json:"permitDeny,omitempty" xml:"permitDeny,omitempty"`
 }
 
 // BuddyListData is the buddylist event payload and the buddy list half of the
@@ -286,7 +286,7 @@ func (h *AimHandler) StartSession(w http.ResponseWriter, r *http.Request) {
 	// Wire buddy list refresher so feedbag SNACs from the OSCAR bridge trigger a buddylist event.
 	// The refresher yields the whole buddylist event payload, not just the groups,
 	// so the session that pushes it does not have to know the payload's shape.
-	session.BuddyListRefresher = func(ctx context.Context) (interface{}, error) {
+	session.BuddyListRefresher = func(ctx context.Context) (any, error) {
 		groups, err := h.BuddyListManager.GetBuddyListForUser(ctx, session)
 		if err != nil {
 			return nil, err
@@ -312,13 +312,13 @@ func (h *AimHandler) StartSession(w http.ResponseWriter, r *http.Request) {
 	// re-renders the identity badge. currentWebState reflects the user's live
 	// presence; PublishedURL reflects the feedbag icon, already updated by the time
 	// the OServiceUserInfoUpdate is relayed.
-	session.MyInfoRefresher = func(ctx context.Context) (interface{}, error) {
+	session.MyInfoRefresher = func(ctx context.Context) (any, error) {
 		icon := h.IconSource.PublishedURL(ctx, session.BaseURL, screenName.IdentScreenName())
 		return buildMyInfo(screenName, currentWebState(session.OSCARSession), icon), nil
 	}
 
 	// Wire permit/deny refresher so FeedbagUpdateItem SNACs trigger a permitDeny event.
-	session.PermitDenyRefresher = func(ctx context.Context) (interface{}, error) {
+	session.PermitDenyRefresher = func(ctx context.Context) (any, error) {
 		frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
 		fb, err := h.FeedbagService.Query(ctx, session.OSCARSession, frame)
 		if err != nil {
@@ -449,7 +449,7 @@ func (h *AimHandler) StartSession(w http.ResponseWriter, r *http.Request) {
 			// populates. Both the block/unblock menu action and the "blocked"
 			// presence state read that model and no-op silently while it is
 			// empty, so the session has to start with one.
-			var pdPayload interface{} = PermitDenyData{PDMode: "permitAll"}
+			var pdPayload any = PermitDenyData{PDMode: "permitAll"}
 			if pdd, err := session.PermitDenyRefresher(ctx); err != nil {
 				h.Logger.ErrorContext(ctx, "failed to get permit/deny settings", "err", err.Error())
 			} else {

+ 7 - 7
server/webapi/events.go

@@ -28,10 +28,10 @@ const (
 
 // Event represents an event to be delivered to a web client.
 type Event struct {
-	Type      EventType   `json:"type" xml:"type"`
-	SeqNum    uint64      `json:"seqNum" xml:"seqNum"`
-	Timestamp int64       `json:"timestamp" xml:"timestamp"`
-	Data      interface{} `json:"eventData" xml:"eventData"`
+	Type      EventType `json:"type" xml:"type"`
+	SeqNum    uint64    `json:"seqNum" xml:"seqNum"`
+	Timestamp int64     `json:"timestamp" xml:"timestamp"`
+	Data      any       `json:"eventData" xml:"eventData"`
 }
 
 // PresenceEvent represents a presence change event.
@@ -156,7 +156,7 @@ type RateLimitClass struct {
 // EventQueue manages a queue of events for a WebAPI session.
 type EventQueue struct {
 	events    []Event
-	seqNum    uint64
+	seqNum    atomic.Uint64
 	maxSize   int
 	mu        sync.RWMutex
 	waitChan  chan struct{}
@@ -185,7 +185,7 @@ func NewEventQueue(maxSize int) *EventQueue {
 }
 
 // Push adds an event to the queue.
-func (q *EventQueue) Push(eventType EventType, data interface{}) {
+func (q *EventQueue) Push(eventType EventType, data any) {
 	if q.isClosed() {
 		return
 	}
@@ -194,7 +194,7 @@ func (q *EventQueue) Push(eventType EventType, data interface{}) {
 	defer q.mu.Unlock()
 
 	// Increment sequence number atomically
-	seqNum := atomic.AddUint64(&q.seqNum, 1)
+	seqNum := q.seqNum.Add(1)
 
 	event := Event{
 		Type:      eventType,

+ 1 - 1
server/webapi/params.go

@@ -86,7 +86,7 @@ func bodyValues(r *http.Request, key string) []string {
 func targetNames(r *http.Request) []string {
 	var targets []string
 	for _, value := range paramValues(r, "t") {
-		for _, name := range strings.Split(value, ",") {
+		for name := range strings.SplitSeq(value, ",") {
 			if name = strings.TrimSpace(name); name != "" {
 				targets = append(targets, name)
 			}

+ 11 - 11
server/webapi/preference_handler.go

@@ -369,7 +369,7 @@ func effectiveBuddyPrefs(list wire.TLVList) *PreferenceData {
 
 // prefFieldIndex maps a preference name to its PreferenceData field.
 var prefFieldIndex = func() map[string]int {
-	t := reflect.TypeOf(PreferenceData{})
+	t := reflect.TypeFor[PreferenceData]()
 	index := make(map[string]int, t.NumField())
 	for i := 0; i < t.NumField(); i++ {
 		name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",")
@@ -419,8 +419,8 @@ func (p *PreferenceData) Map() map[string]any {
 func (p *PreferenceData) Len() int {
 	fields := reflect.ValueOf(p).Elem()
 	n := 0
-	for i := 0; i < fields.NumField(); i++ {
-		if !fields.Field(i).IsNil() {
+	for _, field := range fields.Fields() {
+		if !field.IsNil() {
 			n++
 		}
 	}
@@ -493,8 +493,8 @@ func (h *PreferenceHandler) SetPermitDeny(w http.ResponseWriter, r *http.Request
 
 	// Handle permit list updates
 	if pdAllow := r.URL.Query().Get("pdAllow"); pdAllow != "" {
-		users := strings.Split(pdAllow, ",")
-		for _, user := range users {
+		users := strings.SplitSeq(pdAllow, ",")
+		for user := range users {
 			user = strings.TrimSpace(user)
 			if user != "" {
 				fl.PermitUser(user)
@@ -503,8 +503,8 @@ func (h *PreferenceHandler) SetPermitDeny(w http.ResponseWriter, r *http.Request
 	}
 
 	if pdAllowRemove := r.URL.Query().Get("pdAllowRemove"); pdAllowRemove != "" {
-		users := strings.Split(pdAllowRemove, ",")
-		for _, user := range users {
+		users := strings.SplitSeq(pdAllowRemove, ",")
+		for user := range users {
 			user = strings.TrimSpace(user)
 			if user != "" {
 				fl.DeletePermit(user)
@@ -514,8 +514,8 @@ func (h *PreferenceHandler) SetPermitDeny(w http.ResponseWriter, r *http.Request
 
 	// Handle deny list updates
 	if pdBlock := r.URL.Query().Get("pdBlock"); pdBlock != "" {
-		users := strings.Split(pdBlock, ",")
-		for _, user := range users {
+		users := strings.SplitSeq(pdBlock, ",")
+		for user := range users {
 			user = strings.TrimSpace(user)
 			if user != "" {
 				fl.DenyUser(user)
@@ -524,8 +524,8 @@ func (h *PreferenceHandler) SetPermitDeny(w http.ResponseWriter, r *http.Request
 	}
 
 	if pdBlockRemove := r.URL.Query().Get("pdBlockRemove"); pdBlockRemove != "" {
-		users := strings.Split(pdBlockRemove, ",")
-		for _, user := range users {
+		users := strings.SplitSeq(pdBlockRemove, ",")
+		for user := range users {
 			user = strings.TrimSpace(user)
 			if user != "" {
 				fl.DeleteDeny(user)

+ 3 - 3
server/webapi/presence_handler_test.go

@@ -445,7 +445,7 @@ func TestPresenceHandler_SetState_MyInfoNormalizesAimID(t *testing.T) {
 	// The setState response body carries the same identity fields.
 	var resp struct {
 		Response struct {
-			Data map[string]interface{} `json:"data"`
+			Data map[string]any `json:"data"`
 		} `json:"response"`
 	}
 	assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
@@ -862,7 +862,7 @@ func TestPresenceHandler_GetPresence_TruncatesOversizedTargetList(t *testing.T)
 	}
 
 	query := "/presence/get?aimsid=" + aimsid + "&f=json"
-	for i := 0; i < maxPresenceTargets+8; i++ {
+	for i := range maxPresenceTargets + 8 {
 		query += fmt.Sprintf("&t=user%d", i)
 	}
 	req, _ := http.NewRequest("GET", query, nil)
@@ -881,7 +881,7 @@ func TestPresenceHandler_GetPresence_TruncatesOversizedTargetList(t *testing.T)
 // member-directory search page.
 func searchPageTargets(n int) []string {
 	names := make([]string, 0, n)
-	for i := 0; i < n; i++ {
+	for i := range n {
 		names = append(names, fmt.Sprintf("user%d", i))
 	}
 	return names

+ 10 - 13
server/webapi/response.go

@@ -67,7 +67,7 @@ type ResponseBody struct {
 	// Data is never omitted. Every Web API method sends a data element even when
 	// it carries no payload, and the client dereferences response.data on any
 	// success; SendResponse substitutes an empty object when a handler sets none.
-	Data interface{} `json:"data" xml:"data"`
+	Data any `json:"data" xml:"data"`
 }
 
 // ErrorResponse represents an error response with proper XML/JSON support.
@@ -86,7 +86,7 @@ type ErrorResponse struct {
 		// Data carries an empty object for the same reason the JSONP error path
 		// sends one: a client callback that reaches response.data on a failure
 		// throws a TypeError when it is absent.
-		Data interface{} `json:"data" xml:"data"`
+		Data any `json:"data" xml:"data"`
 	} `json:"response"`
 }
 
@@ -130,7 +130,7 @@ func requestIDFromRequest(r *http.Request) string {
 // the request correlation id, and an empty data object for a response that
 // carries no payload. Both are things every encoder needs and none can infer —
 // and encoding/xml has no way to render a nil data at all.
-func normalizeEnvelope(r *http.Request, data interface{}) interface{} {
+func normalizeEnvelope(r *http.Request, data any) any {
 	br, ok := data.(BaseResponse)
 	if !ok {
 		return data
@@ -146,7 +146,7 @@ func normalizeEnvelope(r *http.Request, data interface{}) interface{} {
 
 // SendResponse sends a response in the requested format (JSON, JSONP, XML, or AMF).
 // This is the centralized function that all handlers should use for responses.
-func SendResponse(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) {
+func SendResponse(w http.ResponseWriter, r *http.Request, data any, logger *slog.Logger) {
 	data = normalizeEnvelope(r, data)
 
 	format := requestFormat(r)
@@ -205,7 +205,7 @@ func SendErrorDetail(w http.ResponseWriter, r *http.Request, httpStatus, statusC
 //
 // Pass nil for a bare acknowledgement: SendResponse substitutes the empty data
 // object the client dereferences unconditionally on success.
-func SendOK(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) {
+func SendOK(w http.ResponseWriter, r *http.Request, data any, logger *slog.Logger) {
 	resp := BaseResponse{}
 	resp.Response.StatusCode = 200
 	resp.Response.StatusText = "Ok"
@@ -321,7 +321,7 @@ func sendXMLError(w http.ResponseWriter, httpStatus int, resp ErrorResponse) {
 }
 
 // sendJSON sends a JSON response.
-func sendJSON(w http.ResponseWriter, data interface{}, logger *slog.Logger) {
+func sendJSON(w http.ResponseWriter, data any, logger *slog.Logger) {
 	w.Header().Set("Content-Type", "application/json")
 	body, err := json.Marshal(data)
 	if err != nil {
@@ -339,7 +339,7 @@ func sendJSON(w http.ResponseWriter, data interface{}, logger *slog.Logger) {
 }
 
 // sendXML sends an XML response.
-func sendXML(w http.ResponseWriter, data interface{}, logger *slog.Logger) {
+func sendXML(w http.ResponseWriter, data any, logger *slog.Logger) {
 	w.Header().Set("Content-Type", "text/xml; charset=utf-8")
 
 	// Every payload is a struct whose xml tags name its elements, and the
@@ -374,7 +374,7 @@ func jsonpCallback(r *http.Request) string {
 }
 
 // sendJSONP sends a JSONP response with the specified callback.
-func sendJSONP(w http.ResponseWriter, r *http.Request, callback string, data interface{}, logger *slog.Logger) {
+func sendJSONP(w http.ResponseWriter, r *http.Request, callback string, data any, logger *slog.Logger) {
 	// Validate callback to prevent XSS. This is the one error here that cannot be
 	// delivered as JSONP: there is no callback name safe to write.
 	if !isValidCallback(callback) {
@@ -420,7 +420,7 @@ func isValidCallback(callback string) bool {
 }
 
 // sendAMF sends an AMF response
-func sendAMF(w http.ResponseWriter, r *http.Request, data interface{}, logger *slog.Logger) {
+func sendAMF(w http.ResponseWriter, r *http.Request, data any, logger *slog.Logger) {
 	amfData, err := wire.MarshalAMF3(data)
 	if err != nil {
 		if logger != nil {
@@ -440,10 +440,7 @@ func sendAMF(w http.ResponseWriter, r *http.Request, data interface{}, logger *s
 	if logger != nil && logger.Enabled(context.TODO(), slog.LevelDebug) {
 		hexPreview := ""
 		if len(amfData) > 0 {
-			previewLen := len(amfData)
-			if previewLen > 64 {
-				previewLen = 64
-			}
+			previewLen := min(len(amfData), 64)
 			hexPreview = hex.EncodeToString(amfData[:previewLen])
 		}
 

+ 3 - 3
server/webapi/server.go

@@ -356,11 +356,11 @@ func (h Handler) GetHelloWorldHandler(w http.ResponseWriter, r *http.Request) {
 	// Must return the same JSON envelope as other Web AIM APIs.
 	h.Logger.Info("webapi root GET", "remote", r.RemoteAddr, "host", r.Host, "path", r.URL.Path)
 	w.Header().Set("Content-Type", "application/json; charset=utf-8")
-	resp := map[string]interface{}{
-		"response": map[string]interface{}{
+	resp := map[string]any{
+		"response": map[string]any{
 			"statusCode": 200,
 			"statusText": "Ok",
-			"data":       map[string]interface{}{},
+			"data":       map[string]any{},
 		},
 	}
 	_ = json.NewEncoder(w).Encode(resp)

+ 23 - 32
server/webapi/session.go

@@ -8,6 +8,7 @@ import (
 	"errors"
 	"log/slog"
 	mrand "math/rand/v2"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -57,23 +58,23 @@ const (
 
 // Session represents an active Web AIM API session.
 type Session struct {
-	AimSID              string                                         // Unique session ID for web client
-	ScreenName          state.DisplayScreenName                        // User identity
-	OSCARSession        *state.SessionInstance                         // Bridge to existing OSCAR session
-	BaseURL             string                                         // Web API base URL advertised to the web client, used to build absolute asset URLs
-	Events              []string                                       // Subscribed event types
-	EventQueue          *EventQueue                                    // Per-session event queue
-	ClientName          string                                         // Client application name
-	ClientVersion       string                                         // Client application version
-	CreatedAt           time.Time                                      // SessionInstance creation time
-	LastAccessed        time.Time                                      // Last activity time
-	ExpiresAt           time.Time                                      // SessionInstance expiration time
-	FetchTimeout        int                                            // Long-polling timeout in milliseconds
-	TimeToNextFetch     int                                            // Suggested delay before next fetch
-	RemoteAddr          string                                         // Client IP address
-	BuddyListRefresher  func(ctx context.Context) (interface{}, error) // Called on feedbag changes to push buddylist event
-	PermitDenyRefresher func(ctx context.Context) (interface{}, error) // Called on feedbag changes to push permitDeny event
-	MyInfoRefresher     func(ctx context.Context) (interface{}, error) // Called on self user-info updates (e.g. icon change) to push myInfo event
+	AimSID              string                                 // Unique session ID for web client
+	ScreenName          state.DisplayScreenName                // User identity
+	OSCARSession        *state.SessionInstance                 // Bridge to existing OSCAR session
+	BaseURL             string                                 // Web API base URL advertised to the web client, used to build absolute asset URLs
+	Events              []string                               // Subscribed event types
+	EventQueue          *EventQueue                            // Per-session event queue
+	ClientName          string                                 // Client application name
+	ClientVersion       string                                 // Client application version
+	CreatedAt           time.Time                              // SessionInstance creation time
+	LastAccessed        time.Time                              // Last activity time
+	ExpiresAt           time.Time                              // SessionInstance expiration time
+	FetchTimeout        int                                    // Long-polling timeout in milliseconds
+	TimeToNextFetch     int                                    // Suggested delay before next fetch
+	RemoteAddr          string                                 // Client IP address
+	BuddyListRefresher  func(ctx context.Context) (any, error) // Called on feedbag changes to push buddylist event
+	PermitDenyRefresher func(ctx context.Context) (any, error) // Called on feedbag changes to push permitDeny event
+	MyInfoRefresher     func(ctx context.Context) (any, error) // Called on self user-info updates (e.g. icon change) to push myInfo event
 	BuddyAliasLoader    func(ctx context.Context) (map[string]string, error)
 	// BuddyIconURL formats the absolute buddyIcon URL for a buddy from the icon
 	// hash carried in a presence SNAC. Returns "" when no URL can be published.
@@ -166,12 +167,7 @@ func (s *Session) Touch() {
 
 // IsSubscribedTo checks if the session is subscribed to a specific event type.
 func (s *Session) IsSubscribedTo(eventType string) bool {
-	for _, event := range s.Events {
-		if event == eventType {
-			return true
-		}
-	}
-	return false
+	return slices.Contains(s.Events, eventType)
 }
 
 // StartListeningToOSCARSession starts a goroutine that listens to the OSCAR session's
@@ -183,9 +179,7 @@ func (s *Session) StartListeningToOSCARSession() {
 		return
 	}
 
-	s.listeners.Add(1)
-	go func() {
-		defer s.listeners.Done()
+	s.listeners.Go(func() {
 		msgCh := s.OSCARSession.ReceiveMessage()
 		for {
 			select {
@@ -209,7 +203,7 @@ func (s *Session) StartListeningToOSCARSession() {
 				return
 			}
 		}
-	}()
+	})
 }
 
 // Close tears down the session: it releases any parked event fetchers, closes
@@ -627,11 +621,8 @@ func (s *Session) handleFeedbagMessage(msg wire.SNACMessage) {
 		if body, ok := msg.Body.(wire.SNAC_0x13_0x0E_FeedbagStatus); ok {
 			// A buddy declined for authorization is not stored, and is simply
 			// absent from the refreshed roster.
-			for _, result := range body.Results {
-				if result == feedbagResultAuthRequired {
-					s.logger.Info("feedbag item declined pending authorization")
-					break
-				}
+			if slices.Contains(body.Results, feedbagResultAuthRequired) {
+				s.logger.Info("feedbag item declined pending authorization")
 			}
 		}
 		s.refreshBuddyList()

+ 7 - 9
server/webapi/session_test.go

@@ -523,7 +523,7 @@ func TestSession_FeedbagSNACRefreshesPermitDeny(t *testing.T) {
 				ScreenName: state.DisplayScreenName("me"),
 				EventQueue: NewEventQueue(10),
 				logger:     slog.New(slog.NewTextHandler(io.Discard, nil)),
-				PermitDenyRefresher: func(_ context.Context) (interface{}, error) {
+				PermitDenyRefresher: func(_ context.Context) (any, error) {
 					return map[string]any{"pdMode": "denySome"}, nil
 				},
 			}
@@ -758,9 +758,9 @@ func TestSession_PushesMyInfoOnUserInfoUpdate(t *testing.T) {
 			Events:     events,
 			EventQueue: NewEventQueue(10),
 			logger:     slog.New(slog.NewTextHandler(io.Discard, nil)),
-			MyInfoRefresher: func(_ context.Context) (interface{}, error) {
+			MyInfoRefresher: func(_ context.Context) (any, error) {
 				refreshes++
-				return map[string]interface{}{"aimId": "me", "buddyIcon": "icon:new"}, nil
+				return map[string]any{"aimId": "me", "buddyIcon": "icon:new"}, nil
 			},
 		}, &refreshes
 	}
@@ -777,7 +777,7 @@ func TestSession_PushesMyInfoOnUserInfoUpdate(t *testing.T) {
 		events := sess.EventQueue.GetAllEvents()
 		require.Len(t, events, 1)
 		assert.Equal(t, "myInfo", string(events[0].Type))
-		assert.Equal(t, "icon:new", events[0].Data.(map[string]interface{})["buddyIcon"])
+		assert.Equal(t, "icon:new", events[0].Data.(map[string]any)["buddyIcon"])
 		assert.Equal(t, 1, *refreshes)
 	})
 
@@ -820,11 +820,9 @@ func TestSessionManager_ShutdownBoundedByContext(t *testing.T) {
 	// Stand in for a listener wedged somewhere that never observes cancellation.
 	release := make(chan struct{})
 	defer close(release)
-	sess.listeners.Add(1)
-	go func() {
-		defer sess.listeners.Done()
+	sess.listeners.Go(func() {
 		<-release
-	}()
+	})
 
 	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
 	defer cancel()
@@ -1084,7 +1082,7 @@ func TestSession_FeedbagStatusRefreshesBuddyList(t *testing.T) {
 				Events:     tt.events,
 				EventQueue: NewEventQueue(10),
 				logger:     slog.New(slog.NewTextHandler(io.Discard, nil)),
-				BuddyListRefresher: func(_ context.Context) (interface{}, error) {
+				BuddyListRefresher: func(_ context.Context) (any, error) {
 					refreshed++
 					return &BuddyListData{Groups: []BuddyGroup{}}, nil
 				},