···6262 }
63636464 // Parse as JSON to verify structure
6565- var logEntry map[string]interface{}
6565+ var logEntry map[string]any
6666 if err := json.Unmarshal([]byte(strings.TrimSpace(logOutput)), &logEntry); err != nil {
6767 t.Fatalf("Failed to parse log as JSON: %v\nOutput: %s", err, logOutput)
6868 }
···7777 }
78787979 // Verify DIDs array is present
8080- didsFromLog, ok := logEntry["dids"].([]interface{})
8080+ didsFromLog, ok := logEntry["dids"].([]any)
8181 if !ok {
8282 t.Fatalf("Expected 'dids' to be an array, got %T", logEntry["dids"])
8383 }
+2-3
internal/atproto/cache.go
···11package atproto
2233import (
44+ "maps"
45 "sync"
56 "time"
67···5354 var dirty map[string]bool
5455 if c.DirtyCollections != nil {
5556 dirty = make(map[string]bool, len(c.DirtyCollections))
5656- for k, v := range c.DirtyCollections {
5757- dirty[k] = v
5858- }
5757+ maps.Copy(dirty, c.DirtyCollections)
5958 }
6059 return &UserCache{
6160 Beans: c.Beans,
+12-12
internal/atproto/cache_test.go
···428428 wg.Add(numGoroutines * 2)
429429430430 // Writers
431431- for i := 0; i < numGoroutines; i++ {
431431+ for i := range numGoroutines {
432432 go func(id int) {
433433 defer wg.Done()
434434- for j := 0; j < numOperations; j++ {
434434+ for range numOperations {
435435 sessionID := "session"
436436 userCache := &UserCache{
437437 Beans: []*models.Bean{{RKey: "bean"}},
···443443 }
444444445445 // Readers
446446- for i := 0; i < numGoroutines; i++ {
446446+ for i := range numGoroutines {
447447 go func(id int) {
448448 defer wg.Done()
449449- for j := 0; j < numOperations; j++ {
449449+ for range numOperations {
450450 cache.Get("session")
451451 }
452452 }(i)
···470470471471 go func() {
472472 defer wg.Done()
473473- for i := 0; i < numOperations; i++ {
473473+ for range numOperations {
474474 cache.SetBeans(sessionID, []*models.Bean{{RKey: "bean"}})
475475 }
476476 }()
477477478478 go func() {
479479 defer wg.Done()
480480- for i := 0; i < numOperations; i++ {
480480+ for range numOperations {
481481 cache.SetRoasters(sessionID, []*models.Roaster{{RKey: "roaster"}})
482482 }
483483 }()
484484485485 go func() {
486486 defer wg.Done()
487487- for i := 0; i < numOperations; i++ {
487487+ for range numOperations {
488488 cache.InvalidateBeans(sessionID)
489489 }
490490 }()
491491492492 go func() {
493493 defer wg.Done()
494494- for i := 0; i < numOperations; i++ {
494494+ for range numOperations {
495495 cache.InvalidateRoasters(sessionID)
496496 }
497497 }()
498498499499 go func() {
500500 defer wg.Done()
501501- for i := 0; i < numOperations; i++ {
501501+ for range numOperations {
502502 cache.Get(sessionID)
503503 }
504504 }()
···514514 // Writer
515515 go func() {
516516 defer wg.Done()
517517- for i := 0; i < numOperations; i++ {
517517+ for range numOperations {
518518 cache.Set("session", &UserCache{
519519 Beans: []*models.Bean{{RKey: "bean"}},
520520 Timestamp: time.Now(),
···525525 // Reader
526526 go func() {
527527 defer wg.Done()
528528- for i := 0; i < numOperations; i++ {
528528+ for range numOperations {
529529 cache.Get("session")
530530 }
531531 }()
···533533 // Cleanup
534534 go func() {
535535 defer wg.Done()
536536- for i := 0; i < numOperations; i++ {
536536+ for range numOperations {
537537 cache.Cleanup()
538538 }
539539 }()
+13-13
internal/atproto/client.go
···8282// CreateRecordInput contains parameters for creating a record
8383type CreateRecordInput struct {
8484 Collection string
8585- Record interface{}
8585+ Record any
8686 RKey *string // Optional, if nil a TID will be generated
8787}
8888···104104 }
105105106106 // Build the request body
107107- body := map[string]interface{}{
107107+ body := map[string]any{
108108 "repo": did.String(),
109109 "collection": input.Collection,
110110 "record": input.Record,
···159159type GetRecordOutput struct {
160160 URI string
161161 CID string
162162- Value map[string]interface{}
162162+ Value map[string]any
163163}
164164165165// GetRecord retrieves a single record by its rkey
···182182183183 // Use the API client's Get method to call com.atproto.repo.getRecord
184184 var result struct {
185185- URI string `json:"uri"`
186186- CID string `json:"cid"`
187187- Value map[string]interface{} `json:"value"`
185185+ URI string `json:"uri"`
186186+ CID string `json:"cid"`
187187+ Value map[string]any `json:"value"`
188188 }
189189190190 err = apiClient.Get(ctx, "com.atproto.repo.getRecord", params, &result)
···236236type Record struct {
237237 URI string
238238 CID string
239239- Value map[string]interface{}
239239+ Value map[string]any
240240}
241241242242// ListRecords retrieves a list of records from a collection
···266266 // Use the API client's Get method to call com.atproto.repo.listRecords
267267 var result struct {
268268 Records []struct {
269269- URI string `json:"uri"`
270270- CID string `json:"cid"`
271271- Value map[string]interface{} `json:"value"`
269269+ URI string `json:"uri"`
270270+ CID string `json:"cid"`
271271+ Value map[string]any `json:"value"`
272272 } `json:"records"`
273273 Cursor *string `json:"cursor,omitempty"`
274274 }
···382382type PutRecordInput struct {
383383 Collection string
384384 RKey string
385385- Record interface{}
385385+ Record any
386386}
387387388388// PutRecord updates an existing record in the user's repository
···397397 }
398398399399 // Build the request body
400400- body := map[string]interface{}{
400400+ body := map[string]any{
401401 "repo": did.String(),
402402 "collection": input.Collection,
403403 "rkey": input.RKey,
···456456 }
457457458458 // Build the request body
459459- body := map[string]interface{}{
459459+ body := map[string]any{
460460 "repo": did.String(),
461461 "collection": input.Collection,
462462 "rkey": input.RKey,
+2-2
internal/atproto/public_client.go
···158158 break
159159 }
160160 }
161161- } else if strings.HasPrefix(did, "did:web:") {
161161+ } else if after, ok := strings.CutPrefix(did, "did:web:"); ok {
162162 // Web DID - the domain is the PDS
163163 // Validate domain to prevent SSRF attacks
164164- domain := strings.TrimPrefix(did, "did:web:")
164164+ domain := after
165165 // Handle percent-encoded colons for ports (e.g., did:web:example.com%3A8080)
166166 domain = strings.ReplaceAll(domain, "%3A", ":")
167167
+43-43
internal/atproto/records.go
···11111212// toFloat64 extracts a numeric value from an interface{} that may be int or float64.
1313// JSON decoding produces float64, but in-memory maps may contain int.
1414-func toFloat64(v interface{}) (float64, bool) {
1414+func toFloat64(v any) (float64, bool) {
1515 switch n := v.(type) {
1616 case float64:
1717 return n, true
···2525// ========== Recipe Conversions ==========
26262727// RecipeToRecord converts a models.Recipe to an atproto record map
2828-func RecipeToRecord(recipe *models.Recipe, brewerURI string) (map[string]interface{}, error) {
2929- record := map[string]interface{}{
2828+func RecipeToRecord(recipe *models.Recipe, brewerURI string) (map[string]any, error) {
2929+ record := map[string]any{
3030 "$type": NSIDRecipe,
3131 "name": recipe.Name,
3232 "createdAt": recipe.CreatedAt.Format(time.RFC3339),
···5252 }
53535454 if len(recipe.Pours) > 0 {
5555- pours := make([]map[string]interface{}, len(recipe.Pours))
5555+ pours := make([]map[string]any, len(recipe.Pours))
5656 for i, pour := range recipe.Pours {
5757- pours[i] = map[string]interface{}{
5757+ pours[i] = map[string]any{
5858 "waterAmount": pour.WaterAmount,
5959 "timeSeconds": pour.TimeSeconds,
6060 }
···6666}
67676868// RecordToRecipe converts an atproto record map to a models.Recipe
6969-func RecordToRecipe(record map[string]interface{}, atURI string) (*models.Recipe, error) {
6969+func RecordToRecipe(record map[string]any, atURI string) (*models.Recipe, error) {
7070 recipe := &models.Recipe{}
71717272 if atURI != "" {
···109109 recipe.SourceRef = sourceRef
110110 }
111111112112- if poursRaw, ok := record["pours"].([]interface{}); ok {
112112+ if poursRaw, ok := record["pours"].([]any); ok {
113113 recipe.Pours = make([]*models.Pour, len(poursRaw))
114114 for i, pourRaw := range poursRaw {
115115- pourMap, ok := pourRaw.(map[string]interface{})
115115+ pourMap, ok := pourRaw.(map[string]any)
116116 if !ok {
117117 continue
118118 }
···135135136136// BrewToRecord converts a models.Brew to an atproto record map
137137// Note: References (beanRef, grinderRef, brewerRef, recipeRef) must be AT-URIs
138138-func BrewToRecord(brew *models.Brew, beanURI, grinderURI, brewerURI, recipeURI string) (map[string]interface{}, error) {
138138+func BrewToRecord(brew *models.Brew, beanURI, grinderURI, brewerURI, recipeURI string) (map[string]any, error) {
139139 if beanURI == "" {
140140 return nil, fmt.Errorf("beanRef (AT-URI) is required")
141141 }
142142143143- record := map[string]interface{}{
143143+ record := map[string]any{
144144 "$type": NSIDBrew,
145145 "beanRef": beanURI,
146146 "createdAt": brew.CreatedAt.Format(time.RFC3339),
···184184185185 // Convert pours to embedded array
186186 if len(brew.Pours) > 0 {
187187- pours := make([]map[string]interface{}, len(brew.Pours))
187187+ pours := make([]map[string]any, len(brew.Pours))
188188 for i, pour := range brew.Pours {
189189- pours[i] = map[string]interface{}{
189189+ pours[i] = map[string]any{
190190 "waterAmount": pour.WaterAmount,
191191 "timeSeconds": pour.TimeSeconds,
192192 }
···196196197197 // Espresso-specific params
198198 if brew.EspressoParams != nil {
199199- ep := map[string]interface{}{}
199199+ ep := map[string]any{}
200200 if brew.EspressoParams.YieldWeight > 0 {
201201 ep["yieldWeight"] = int(brew.EspressoParams.YieldWeight * 10) // tenths of a gram
202202 }
···213213214214 // Pour-over-specific params
215215 if brew.PouroverParams != nil {
216216- pp := map[string]interface{}{}
216216+ pp := map[string]any{}
217217 if brew.PouroverParams.BloomWater > 0 {
218218 pp["bloomWater"] = brew.PouroverParams.BloomWater
219219 }
···236236237237// RecordToBrew converts an atproto record map to a models.Brew
238238// The atURI parameter should be the full AT-URI of this brew record
239239-func RecordToBrew(record map[string]interface{}, atURI string) (*models.Brew, error) {
239239+func RecordToBrew(record map[string]any, atURI string) (*models.Brew, error) {
240240 brew := &models.Brew{}
241241242242 // Extract rkey from AT-URI
···295295 }
296296297297 // Convert pours from embedded array
298298- if poursRaw, ok := record["pours"].([]interface{}); ok {
298298+ if poursRaw, ok := record["pours"].([]any); ok {
299299 brew.Pours = make([]*models.Pour, len(poursRaw))
300300 for i, pourRaw := range poursRaw {
301301- pourMap, ok := pourRaw.(map[string]interface{})
301301+ pourMap, ok := pourRaw.(map[string]any)
302302 if !ok {
303303 continue
304304 }
···315315 }
316316317317 // Espresso params
318318- if epRaw, ok := record["espressoParams"].(map[string]interface{}); ok {
318318+ if epRaw, ok := record["espressoParams"].(map[string]any); ok {
319319 ep := &models.EspressoParams{}
320320 if v, ok := toFloat64(epRaw["yieldWeight"]); ok {
321321 ep.YieldWeight = v / 10.0
···330330 }
331331332332 // Pour-over params
333333- if ppRaw, ok := record["pouroverParams"].(map[string]interface{}); ok {
333333+ if ppRaw, ok := record["pouroverParams"].(map[string]any); ok {
334334 pp := &models.PouroverParams{}
335335 if v, ok := toFloat64(ppRaw["bloomWater"]); ok {
336336 pp.BloomWater = int(v)
···353353// ========== Bean Conversions ==========
354354355355// BeanToRecord converts a models.Bean to an atproto record map
356356-func BeanToRecord(bean *models.Bean, roasterURI string) (map[string]interface{}, error) {
357357- record := map[string]interface{}{
356356+func BeanToRecord(bean *models.Bean, roasterURI string) (map[string]any, error) {
357357+ record := map[string]any{
358358 "$type": NSIDBean,
359359 "name": bean.Name,
360360 "createdAt": bean.CreatedAt.Format(time.RFC3339),
···392392}
393393394394// RecordToBean converts an atproto record map to a models.Bean
395395-func RecordToBean(record map[string]interface{}, atURI string) (*models.Bean, error) {
395395+func RecordToBean(record map[string]any, atURI string) (*models.Bean, error) {
396396 bean := &models.Bean{}
397397398398 // Extract rkey from AT-URI
···455455// ========== Roaster Conversions ==========
456456457457// RoasterToRecord converts a models.Roaster to an atproto record map
458458-func RoasterToRecord(roaster *models.Roaster) (map[string]interface{}, error) {
459459- record := map[string]interface{}{
458458+func RoasterToRecord(roaster *models.Roaster) (map[string]any, error) {
459459+ record := map[string]any{
460460 "$type": NSIDRoaster,
461461 "name": roaster.Name,
462462 "createdAt": roaster.CreatedAt.Format(time.RFC3339),
···477477}
478478479479// RecordToRoaster converts an atproto record map to a models.Roaster
480480-func RecordToRoaster(record map[string]interface{}, atURI string) (*models.Roaster, error) {
480480+func RecordToRoaster(record map[string]any, atURI string) (*models.Roaster, error) {
481481 roaster := &models.Roaster{}
482482483483 // Extract rkey from AT-URI
···524524// ========== Grinder Conversions ==========
525525526526// GrinderToRecord converts a models.Grinder to an atproto record map
527527-func GrinderToRecord(grinder *models.Grinder) (map[string]interface{}, error) {
528528- record := map[string]interface{}{
527527+func GrinderToRecord(grinder *models.Grinder) (map[string]any, error) {
528528+ record := map[string]any{
529529 "$type": NSIDGrinder,
530530 "name": grinder.Name,
531531 "createdAt": grinder.CreatedAt.Format(time.RFC3339),
···549549}
550550551551// RecordToGrinder converts an atproto record map to a models.Grinder
552552-func RecordToGrinder(record map[string]interface{}, atURI string) (*models.Grinder, error) {
552552+func RecordToGrinder(record map[string]any, atURI string) (*models.Grinder, error) {
553553 grinder := &models.Grinder{}
554554555555 // Extract rkey from AT-URI
···599599// ========== Brewer Conversions ==========
600600601601// BrewerToRecord converts a models.Brewer to an atproto record map
602602-func BrewerToRecord(brewer *models.Brewer) (map[string]interface{}, error) {
603603- record := map[string]interface{}{
602602+func BrewerToRecord(brewer *models.Brewer) (map[string]any, error) {
603603+ record := map[string]any{
604604 "$type": NSIDBrewer,
605605 "name": brewer.Name,
606606 "createdAt": brewer.CreatedAt.Format(time.RFC3339),
···621621}
622622623623// RecordToBrewer converts an atproto record map to a models.Brewer
624624-func RecordToBrewer(record map[string]interface{}, atURI string) (*models.Brewer, error) {
624624+func RecordToBrewer(record map[string]any, atURI string) (*models.Brewer, error) {
625625 brewer := &models.Brewer{}
626626627627 // Extract rkey from AT-URI
···669669670670// LikeToRecord converts a models.Like to an atproto record map
671671// Uses com.atproto.repo.strongRef format for the subject
672672-func LikeToRecord(like *models.Like) (map[string]interface{}, error) {
672672+func LikeToRecord(like *models.Like) (map[string]any, error) {
673673 if like.SubjectURI == "" {
674674 return nil, fmt.Errorf("subject URI is required")
675675 }
···677677 return nil, fmt.Errorf("subject CID is required")
678678 }
679679680680- record := map[string]interface{}{
680680+ record := map[string]any{
681681 "$type": NSIDLike,
682682- "subject": map[string]interface{}{
682682+ "subject": map[string]any{
683683 "uri": like.SubjectURI,
684684 "cid": like.SubjectCID,
685685 },
···690690}
691691692692// RecordToLike converts an atproto record map to a models.Like
693693-func RecordToLike(record map[string]interface{}, atURI string) (*models.Like, error) {
693693+func RecordToLike(record map[string]any, atURI string) (*models.Like, error) {
694694 like := &models.Like{}
695695696696 // Extract rkey from AT-URI
···703703 }
704704705705 // Required field: subject (strongRef)
706706- subject, ok := record["subject"].(map[string]interface{})
706706+ subject, ok := record["subject"].(map[string]any)
707707 if !ok {
708708 return nil, fmt.Errorf("subject is required")
709709 }
···737737738738// CommentToRecord converts a models.Comment to an atproto record map
739739// Uses com.atproto.repo.strongRef format for the subject
740740-func CommentToRecord(comment *models.Comment) (map[string]interface{}, error) {
740740+func CommentToRecord(comment *models.Comment) (map[string]any, error) {
741741 if comment.SubjectURI == "" {
742742 return nil, fmt.Errorf("subject URI is required")
743743 }
···748748 return nil, fmt.Errorf("text is required")
749749 }
750750751751- record := map[string]interface{}{
751751+ record := map[string]any{
752752 "$type": NSIDComment,
753753- "subject": map[string]interface{}{
753753+ "subject": map[string]any{
754754 "uri": comment.SubjectURI,
755755 "cid": comment.SubjectCID,
756756 },
···760760761761 // Add optional parent reference for replies
762762 if comment.ParentURI != "" && comment.ParentCID != "" {
763763- record["parent"] = map[string]interface{}{
763763+ record["parent"] = map[string]any{
764764 "uri": comment.ParentURI,
765765 "cid": comment.ParentCID,
766766 }
···770770}
771771772772// RecordToComment converts an atproto record map to a models.Comment
773773-func RecordToComment(record map[string]interface{}, atURI string) (*models.Comment, error) {
773773+func RecordToComment(record map[string]any, atURI string) (*models.Comment, error) {
774774 comment := &models.Comment{}
775775776776 // Extract rkey from AT-URI
···783783 }
784784785785 // Required field: subject (strongRef)
786786- subject, ok := record["subject"].(map[string]interface{})
786786+ subject, ok := record["subject"].(map[string]any)
787787 if !ok {
788788 return nil, fmt.Errorf("subject is required")
789789 }
···818818 comment.CreatedAt = createdAt
819819820820 // Optional field: parent (strongRef for replies)
821821- if parent, ok := record["parent"].(map[string]interface{}); ok {
821821+ if parent, ok := record["parent"].(map[string]any); ok {
822822 if parentURI, ok := parent["uri"].(string); ok && parentURI != "" {
823823 comment.ParentURI = parentURI
824824 }
···4949}
50505151// witnessRecordToMap is a package-internal alias for WitnessRecordToMap.
5252-func witnessRecordToMap(wr *WitnessRecord) (map[string]interface{}, error) {
5252+func witnessRecordToMap(wr *WitnessRecord) (map[string]any, error) {
5353 return WitnessRecordToMap(wr)
5454}
5555···101101// writeThroughWitness upserts a record into the witness cache after a
102102// successful PDS write so subsequent reads see the latest data without
103103// waiting for the firehose to re-index.
104104-func (s *AtprotoStore) writeThroughWitness(collection, rkey, cid string, record interface{}) {
104104+func (s *AtprotoStore) writeThroughWitness(collection, rkey, cid string, record any) {
105105 if s.witnessCache == nil {
106106 return
107107 }
···146146// resolveBrewRefsFromWitness resolves a brew's references (bean, grinder, brewer, recipe)
147147// entirely from the witness cache, avoiding any PDS calls. Falls back to PDS-based resolution
148148// only if a witness lookup fails for any referenced record.
149149-func (s *AtprotoStore) resolveBrewRefsFromWitness(ctx context.Context, brew *models.Brew, record map[string]interface{}) {
149149+func (s *AtprotoStore) resolveBrewRefsFromWitness(ctx context.Context, brew *models.Brew, record map[string]any) {
150150 // Resolve bean (and its roaster)
151151 if beanRef, _ := record["beanRef"].(string); beanRef != "" {
152152 if beanWR := s.getWitnessRecordByURI(ctx, beanRef); beanWR != nil {
···227227// ========== Brew Helpers ==========
228228229229// ExtractBrewRefRKeys extracts rkeys from AT-URI references in a brew record's raw values.
230230-func ExtractBrewRefRKeys(brew *models.Brew, record map[string]interface{}) {
230230+func ExtractBrewRefRKeys(brew *models.Brew, record map[string]any) {
231231 if beanRef, _ := record["beanRef"].(string); beanRef != "" {
232232 if c, err := ResolveATURI(beanRef); err == nil {
233233 brew.BeanRKey = c.RKey
+2-2
internal/atproto/witness.go
···20202121// WitnessRecordToMap unmarshals a WitnessRecord's raw JSON into the map format
2222// expected by the Record* conversion functions.
2323-func WitnessRecordToMap(wr *WitnessRecord) (map[string]interface{}, error) {
2424- var m map[string]interface{}
2323+func WitnessRecordToMap(wr *WitnessRecord) (map[string]any, error) {
2424+ var m map[string]any
2525 if err := json.Unmarshal(wr.Record, &m); err != nil {
2626 return nil, err
2727 }
+12-14
internal/firehose/consumer.go
···84848585// Start begins consuming events in a background goroutine
8686func (c *Consumer) Start(ctx context.Context) {
8787- c.wg.Add(1)
8888- go func() {
8989- defer c.wg.Done()
8787+ c.wg.Go(func() {
9088 c.run(ctx)
9191- }()
8989+ })
9290}
93919492// Stop gracefully stops the consumer
···343341344342 // Special handling for likes - index for counts
345343 if commit.Collection == "social.arabica.alpha.like" {
346346- var recordData map[string]interface{}
344344+ var recordData map[string]any
347345 if err := json.Unmarshal(commit.Record, &recordData); err == nil {
348348- if subject, ok := recordData["subject"].(map[string]interface{}); ok {
346346+ if subject, ok := recordData["subject"].(map[string]any); ok {
349347 if subjectURI, ok := subject["uri"].(string); ok {
350348 if err := c.index.UpsertLike(context.Background(), event.DID, commit.RKey, subjectURI); err != nil {
351349 log.Warn().Err(err).Str("did", event.DID).Str("subject", subjectURI).Msg("failed to index like")
···359357360358 // Special handling for comments - index for counts and retrieval
361359 if commit.Collection == "social.arabica.alpha.comment" {
362362- var recordData map[string]interface{}
360360+ var recordData map[string]any
363361 if err := json.Unmarshal(commit.Record, &recordData); err == nil {
364364- if subject, ok := recordData["subject"].(map[string]interface{}); ok {
362362+ if subject, ok := recordData["subject"].(map[string]any); ok {
365363 if subjectURI, ok := subject["uri"].(string); ok {
366364 text, _ := recordData["text"].(string)
367365 var createdAt time.Time
···376374 }
377375 // Extract optional parent URI for threading
378376 var parentURI string
379379- if parent, ok := recordData["parent"].(map[string]interface{}); ok {
377377+ if parent, ok := recordData["parent"].(map[string]any); ok {
380378 parentURI, _ = parent["uri"].(string)
381379 }
382380 if err := c.index.UpsertComment(context.Background(), event.DID, commit.RKey, subjectURI, parentURI, commit.CID, text, createdAt); err != nil {
···397395 context.Background(),
398396 fmt.Sprintf("at://%s/%s/%s", event.DID, commit.Collection, commit.RKey),
399397 ); err == nil && existingRecord != nil {
400400- var recordData map[string]interface{}
398398+ var recordData map[string]any
401399 if err := json.Unmarshal(existingRecord.Record, &recordData); err == nil {
402402- if subject, ok := recordData["subject"].(map[string]interface{}); ok {
400400+ if subject, ok := recordData["subject"].(map[string]any); ok {
403401 if subjectURI, ok := subject["uri"].(string); ok {
404402 if err := c.index.DeleteLike(context.Background(), event.DID, subjectURI); err != nil {
405403 log.Warn().Err(err).Str("did", event.DID).Str("subject", subjectURI).Msg("failed to delete like index")
···418416 context.Background(),
419417 fmt.Sprintf("at://%s/%s/%s", event.DID, commit.Collection, commit.RKey),
420418 ); err == nil && existingRecord != nil {
421421- var recordData map[string]interface{}
419419+ var recordData map[string]any
422420 if err := json.Unmarshal(existingRecord.Record, &recordData); err == nil {
423423- if subject, ok := recordData["subject"].(map[string]interface{}); ok {
421421+ if subject, ok := recordData["subject"].(map[string]any); ok {
424422 if subjectURI, ok := subject["uri"].(string); ok {
425423 var parentURI string
426426- if parent, ok := recordData["parent"].(map[string]interface{}); ok {
424424+ if parent, ok := recordData["parent"].(map[string]any); ok {
427425 parentURI, _ = parent["uri"].(string)
428426 }
429427 if err := c.index.DeleteComment(context.Background(), event.DID, commit.RKey, subjectURI); err != nil {
+4-7
internal/firehose/index.go
···1476147614771477 switch collection {
14781478 case atproto.NSIDLike:
14791479- if subject, ok := record.Value["subject"].(map[string]interface{}); ok {
14791479+ if subject, ok := record.Value["subject"].(map[string]any); ok {
14801480 if subjectURI, ok := subject["uri"].(string); ok {
14811481 if err := idx.UpsertLike(ctx, did, rkey, subjectURI); err != nil {
14821482 log.Warn().Err(err).Str("uri", record.URI).Msg("failed to index like during backfill")
···14841484 }
14851485 }
14861486 case atproto.NSIDComment:
14871487- if subject, ok := record.Value["subject"].(map[string]interface{}); ok {
14871487+ if subject, ok := record.Value["subject"].(map[string]any); ok {
14881488 if subjectURI, ok := subject["uri"].(string); ok {
14891489 text, _ := record.Value["text"].(string)
14901490 var createdAt time.Time
···14981498 createdAt = time.Now()
14991499 }
15001500 var parentURI string
15011501- if parent, ok := record.Value["parent"].(map[string]interface{}); ok {
15011501+ if parent, ok := record.Value["parent"].(map[string]any); ok {
15021502 parentURI, _ = parent["uri"].(string)
15031503 }
15041504 if err := idx.UpsertComment(ctx, did, rkey, subjectURI, parentURI, record.CID, text, createdAt); err != nil {
···18411841 if limit > 0 && len(result) >= limit {
18421842 return
18431843 }
18441844- visualDepth := depth
18451845- if visualDepth > 2 {
18461846- visualDepth = 2
18471847- }
18441844+ visualDepth := min(depth, 2)
18481845 comment.Depth = visualDepth
18491846 result = append(result, *comment)
18501847
+11-11
internal/firehose/index_test.go
···167167 // Create a chain of comments: depth 0 -> 1 -> 2 -> 3 -> 4
168168 now := time.Now()
169169 parentURI := ""
170170- for i := 0; i < 5; i++ {
170170+ for i := range 5 {
171171 rkey := "comment" + string(rune('A'+i))
172172 err = idx.UpsertComment(ctx, "did:plc:user", rkey, subjectURI, parentURI, "cid"+rkey, "Comment", now.Add(time.Duration(i)*time.Second))
173173 assert.NoError(t, err)
···273273274274 // User1 rates bean1: 6, 8
275275 for i, rating := range []int{6, 8} {
276276- record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":%d,"createdAt":"2025-01-01T00:00:00Z"}`, bean1, rating))
276276+ record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":%d,"createdAt":"2025-01-01T00:00:00Z"}`, bean1, rating)
277277 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user1", "social.arabica.alpha.brew", fmt.Sprintf("u1b1_%d", i), "cid", record, now))
278278 }
279279280280 // User2 rates bean1: 10
281281- record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":10,"createdAt":"2025-01-01T00:00:00Z"}`, bean1))
281281+ record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":10,"createdAt":"2025-01-01T00:00:00Z"}`, bean1)
282282 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user2", "social.arabica.alpha.brew", "u2b1_0", "cid", record, now))
283283284284 // User1 rates bean2: 4
285285- record = []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":4,"createdAt":"2025-01-01T00:00:00Z"}`, bean2))
285285+ record = fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":4,"createdAt":"2025-01-01T00:00:00Z"}`, bean2)
286286 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user1", "social.arabica.alpha.brew", "u1b2_0", "cid", record, now))
287287288288 // Per-user1: bean1 avg=7, bean2 avg=4
···312312 beanURI := "at://did:plc:user1/social.arabica.alpha.bean/bean1"
313313314314 // Brew with rating
315315- record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":7,"createdAt":"2025-01-01T00:00:00Z"}`, beanURI))
315315+ record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":7,"createdAt":"2025-01-01T00:00:00Z"}`, beanURI)
316316 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user1", "social.arabica.alpha.brew", "brew1", "cid", record, now))
317317318318 // Brew without rating
319319- record = []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","createdAt":"2025-01-02T00:00:00Z"}`, beanURI))
319319+ record = fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","createdAt":"2025-01-02T00:00:00Z"}`, beanURI)
320320 assert.NoError(t, idx.UpsertRecord(ctx, "did:plc:user1", "social.arabica.alpha.brew", "brew2", "cid", record, now))
321321322322 stats := idx.AvgBrewRatingByBeanURI(ctx, "")
···338338 roasterURI := "at://did:plc:user1/social.arabica.alpha.roaster/roaster1"
339339340340 // Insert the bean record with roaster reference
341341- beanRecord := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.bean","name":"Ethiopia Yirgacheffe","roasterRef":"%s","createdAt":"2025-01-01T00:00:00Z"}`, roasterURI))
341341+ beanRecord := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.bean","name":"Ethiopia Yirgacheffe","roasterRef":"%s","createdAt":"2025-01-01T00:00:00Z"}`, roasterURI)
342342 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.bean", "bean1", "cid", beanRecord, now))
343343344344 // Insert brews referencing that bean with ratings
345345 for i, rating := range []int{6, 8, 10} {
346346- record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":%d,"createdAt":"2025-01-0%dT00:00:00Z"}`, beanURI, rating, i+1))
346346+ record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":%d,"createdAt":"2025-01-0%dT00:00:00Z"}`, beanURI, rating, i+1)
347347 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.brew", fmt.Sprintf("brew%d", i), "cid", record, now))
348348 }
349349···375375376376 // Two beans from the same roaster
377377 for _, b := range []struct{ uri, rkey string }{{bean1URI, "bean1"}, {bean2URI, "bean2"}} {
378378- record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.bean","name":"Bean","roasterRef":"%s","createdAt":"2025-01-01T00:00:00Z"}`, roasterURI))
378378+ record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.bean","name":"Bean","roasterRef":"%s","createdAt":"2025-01-01T00:00:00Z"}`, roasterURI)
379379 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.bean", b.rkey, "cid", record, now))
380380 }
381381382382 // Brews: bean1 rated 6, bean2 rated 10
383383- record := []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":6,"createdAt":"2025-01-01T00:00:00Z"}`, bean1URI))
383383+ record := fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":6,"createdAt":"2025-01-01T00:00:00Z"}`, bean1URI)
384384 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.brew", "brew1", "cid", record, now))
385385- record = []byte(fmt.Sprintf(`{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":10,"createdAt":"2025-01-01T00:00:00Z"}`, bean2URI))
385385+ record = fmt.Appendf(nil, `{"$type":"social.arabica.alpha.brew","beanRef":"%s","rating":10,"createdAt":"2025-01-01T00:00:00Z"}`, bean2URI)
386386 assert.NoError(t, idx.UpsertRecord(ctx, did, "social.arabica.alpha.brew", "brew2", "cid", record, now))
387387388388 stats := idx.AvgBrewRatingByRoasterURI(ctx, "")
+3-3
internal/firehose/notifications_test.go
···100100 assert.Equal(t, 0, idx.GetUnreadCount(targetDID))
101101102102 // Add some notifications
103103- for i := 0; i < 3; i++ {
103103+ for i := range 3 {
104104 notif := models.Notification{
105105 Type: models.NotificationLike,
106106 ActorDID: "did:plc:actor" + string(rune('a'+i)),
···120120 baseTime := time.Now().Add(-time.Minute) // use past times to avoid race
121121122122 // Add notifications
123123- for i := 0; i < 3; i++ {
123123+ for i := range 3 {
124124 notif := models.Notification{
125125 Type: models.NotificationLike,
126126 ActorDID: "did:plc:actor" + string(rune('a'+i)),
···152152 baseTime := time.Now().Add(-time.Minute)
153153154154 // Add 5 notifications
155155- for i := 0; i < 5; i++ {
155155+ for i := range 5 {
156156 notif := models.Notification{
157157 Type: models.NotificationLike,
158158 ActorDID: "did:plc:actor" + string(rune('a'+i)),
+5-5
internal/handlers/auth.go
···311311 log.Warn().Err(err).Str("did", resolveResult.DID).Msg("Failed to fetch profile")
312312 // Return just the DID if we can't get the profile
313313 w.Header().Set("Content-Type", "application/json")
314314- if err := json.NewEncoder(w).Encode(map[string]interface{}{
314314+ if err := json.NewEncoder(w).Encode(map[string]any{
315315 "did": resolveResult.DID,
316316 "handle": handle,
317317 }); err != nil {
···324324 if profileResp.StatusCode != 200 {
325325 // Return just the DID if we can't get the profile
326326 w.Header().Set("Content-Type", "application/json")
327327- if err := json.NewEncoder(w).Encode(map[string]interface{}{
327327+ if err := json.NewEncoder(w).Encode(map[string]any{
328328 "did": resolveResult.DID,
329329 "handle": handle,
330330 }); err != nil {
···343343 log.Warn().Err(err).Str("did", resolveResult.DID).Msg("Failed to decode profile")
344344 // Return just the DID if we can't parse the profile
345345 w.Header().Set("Content-Type", "application/json")
346346- if err := json.NewEncoder(w).Encode(map[string]interface{}{
346346+ if err := json.NewEncoder(w).Encode(map[string]any{
347347 "did": resolveResult.DID,
348348 "handle": handle,
349349 }); err != nil {
···400400 Msg("Unexpected status searching actors")
401401 // Return empty results instead of error
402402 w.Header().Set("Content-Type", "application/json")
403403- if err := json.NewEncoder(w).Encode(map[string]interface{}{"actors": []interface{}{}}); err != nil {
403403+ if err := json.NewEncoder(w).Encode(map[string]any{"actors": []any{}}); err != nil {
404404 log.Error().Err(err).Msg("Failed to encode empty actors response")
405405 }
406406 return
···419419 log.Warn().Err(err).Str("query", query).Msg("Failed to decode search response")
420420 // Return empty results instead of error
421421 w.Header().Set("Content-Type", "application/json")
422422- if err := json.NewEncoder(w).Encode(map[string]interface{}{"actors": []interface{}{}}); err != nil {
422422+ if err := json.NewEncoder(w).Encode(map[string]any{"actors": []any{}}); err != nil {
423423 log.Error().Err(err).Msg("Failed to encode empty actors response")
424424 }
425425 return
+3-3
internal/handlers/brew.go
···318318}
319319320320// resolveBrewReferences resolves bean, grinder, and brewer references for a brew
321321-func (h *Handler) resolveBrewReferences(ctx context.Context, brew *models.Brew, ownerDID string, record map[string]interface{}) error {
321321+func (h *Handler) resolveBrewReferences(ctx context.Context, brew *models.Brew, ownerDID string, record map[string]any) error {
322322 publicClient := atproto.NewPublicClient()
323323324324 // Resolve bean reference
···366366367367// resolveBrewRefsFromWitness resolves a brew's references (bean, grinder, brewer, recipe)
368368// from the witness cache, avoiding PDS calls for public brew views.
369369-func (h *Handler) resolveBrewRefsFromWitness(ctx context.Context, brew *models.Brew, ownerDID string, record map[string]interface{}) {
369369+func (h *Handler) resolveBrewRefsFromWitness(ctx context.Context, brew *models.Brew, ownerDID string, record map[string]any) {
370370 if h.witnessCache == nil {
371371 return
372372 }
···543543func parsePours(r *http.Request) []models.CreatePourData {
544544 var pours []models.CreatePourData
545545546546- for i := 0; i < maxPours; i++ {
546546+ for i := range maxPours {
547547 waterKey := "pour_water_" + strconv.Itoa(i)
548548 timeKey := "pour_time_" + strconv.Itoa(i)
549549
···177177// decodeRequest decodes either JSON or form data into the target interface based on Content-Type.
178178// The parseForm function is called when the request is form-encoded (not JSON).
179179// Returns an error if parsing fails.
180180-func decodeRequest(r *http.Request, target interface{}, parseForm func() error) error {
180180+func decodeRequest(r *http.Request, target any, parseForm func() error) error {
181181 if isJSONRequest(r) {
182182 // Parse as JSON
183183 if err := json.NewDecoder(r.Body).Decode(target); err != nil {
···208208}
209209210210// writeJSON encodes and writes a JSON response
211211-func writeJSON(w http.ResponseWriter, v interface{}, entityName string) {
211211+func writeJSON(w http.ResponseWriter, v any, entityName string) {
212212 w.Header().Set("Content-Type", "application/json")
213213 if err := json.NewEncoder(w).Encode(v); err != nil {
214214 log.Error().Err(err).Msg("Failed to encode " + entityName + " response")
+3-3
internal/handlers/recipe.go
···504504 type parsedRecord struct {
505505 uri string
506506 did string
507507- data map[string]interface{}
507507+ data map[string]any
508508 recipe *models.Recipe
509509 sourceRef string
510510 sourceDID string
···512512 }
513513 parsed := make([]parsedRecord, 0, len(records))
514514 for i := range records {
515515- var recordData map[string]interface{}
515515+ var recordData map[string]any
516516 if err := json.Unmarshal(records[i].Record, &recordData); err != nil {
517517 continue
518518 }
···587587 recipe.BrewerRKey = c.RKey
588588 }
589589 if brewerRec, ok := brewerRecords[brewerRef]; ok {
590590- var brewerData map[string]interface{}
590590+ var brewerData map[string]any
591591 if err := json.Unmarshal(brewerRec.Record, &brewerData); err == nil {
592592 if brewer, err := atproto.RecordToBrewer(brewerData, brewerRef); err == nil {
593593 recipe.BrewerObj = brewer
+2-2
internal/handlers/testutil.go
···133133)
134134135135// NewAuthenticatedRequest creates a request with authentication context
136136-func NewAuthenticatedRequest(method, path string, body interface{}) *http.Request {
136136+func NewAuthenticatedRequest(method, path string, body any) *http.Request {
137137 req := httptest.NewRequest(method, path, nil)
138138139139 // Add authenticated DID to context using the same keys as OAuth middleware
···150150151151// AssertResponseCode checks if the response has the expected status code
152152func AssertResponseCode(t interface {
153153- Errorf(format string, args ...interface{})
153153+ Errorf(format string, args ...any)
154154}, rec *httptest.ResponseRecorder, expected int) {
155155 if rec.Code != expected {
156156 t.Errorf("Expected status code %d, got %d. Body: %s", expected, rec.Code, rec.Body.String())
+2-2
internal/middleware/logging.go
···1919 // Check X-Forwarded-For header (can contain multiple IPs: client, proxy1, proxy2)
2020 if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
2121 // Take the first IP (the original client)
2222- if idx := strings.Index(xff, ","); idx != -1 {
2323- return strings.TrimSpace(xff[:idx])
2222+ if before, _, ok := strings.Cut(xff, ","); ok {
2323+ return strings.TrimSpace(before)
2424 }
2525 return strings.TrimSpace(xff)
2626 }
+5-5
internal/middleware/security_test.go
···65656666 wrapped := SecurityHeadersMiddleware(handler)
67676868- for i := 0; i < 10; i++ {
6868+ for range 10 {
6969 req := httptest.NewRequest(http.MethodGet, "/", nil)
7070 rec := httptest.NewRecorder()
7171 wrapped.ServeHTTP(rec, req)
···145145 wrapped := middleware(handler)
146146147147 t.Run("auth endpoints use auth limiter", func(t *testing.T) {
148148- for i := 0; i < 2; i++ {
148148+ for range 2 {
149149 req := httptest.NewRequest(http.MethodPost, "/auth/login", nil)
150150 req.RemoteAddr = "1.1.1.1:1234"
151151 rec := httptest.NewRecorder()
···162162 })
163163164164 t.Run("api endpoints use api limiter", func(t *testing.T) {
165165- for i := 0; i < 3; i++ {
165165+ for range 3 {
166166 req := httptest.NewRequest(http.MethodGet, "/api/brews", nil)
167167 req.RemoteAddr = "2.2.2.2:1234"
168168 rec := httptest.NewRecorder()
···178178 })
179179180180 t.Run("other endpoints use global limiter", func(t *testing.T) {
181181- for i := 0; i < 5; i++ {
181181+ for range 5 {
182182 req := httptest.NewRequest(http.MethodGet, "/brews", nil)
183183 req.RemoteAddr = "3.3.3.3:1234"
184184 rec := httptest.NewRecorder()
···194194 })
195195196196 t.Run("login path uses auth limiter", func(t *testing.T) {
197197- for i := 0; i < 2; i++ {
197197+ for range 2 {
198198 req := httptest.NewRequest(http.MethodPost, "/login", nil)
199199 req.RemoteAddr = "4.4.4.4:1234"
200200 rec := httptest.NewRecorder()
+3-6
internal/moderation/models.go
···11package moderation
2233+import "slices"
44+35import "time"
4657// Permission represents a moderation action that can be performed
···47494850// HasPermission checks if this role has the given permission
4951func (r *Role) HasPermission(perm Permission) bool {
5050- for _, p := range r.Permissions {
5151- if p == perm {
5252- return true
5353- }
5454- }
5555- return false
5252+ return slices.Contains(r.Permissions, perm)
5653}
57545855// ModeratorUser represents a user with moderation privileges