A community based topic aggregation platform built on atproto

fix: populate community handle in comment post views

- Capture community.Handle when fetching community data
- Set Handle field in CommunityRef struct
- Improve error handling for missing communities:
- Log as ERROR (not warning) for data integrity issues
- Use DID as fallback for handle/name to prevent API breakage
- Surfaces orphaned post issues in logs while maintaining resilience

Fixes: Community handle field empty in post views from comment service

+47 -33
+47 -33
internal/core/comments/comment_service.go
··· 458 } 459 460 // Build community reference - fetch community to get name and avatar (required by lexicon) 461 - // The lexicon marks communityRef.name as required, so DIDs are insufficient 462 - communityName := post.CommunityDID // Fallback if community not found 463 - var avatarURL *string 464 - 465 - if community, err := s.communityRepo.GetByDID(ctx, post.CommunityDID); err == nil { 466 - // Use display name if available, otherwise fall back to handle or short name 467 - if community.DisplayName != "" { 468 - communityName = community.DisplayName 469 - } else if community.Name != "" { 470 - communityName = community.Name 471 - } else { 472 - communityName = community.Handle 473 } 474 475 - // Build avatar URL from CID if available 476 - // Avatar is stored as blob in community's repository 477 - // Format: https://{pds}/xrpc/com.atproto.sync.getBlob?did={community_did}&cid={avatar_cid} 478 - if community.AvatarCID != "" && community.PDSURL != "" { 479 - // Validate HTTPS for security (prevent mixed content warnings, MitM attacks) 480 - if !strings.HasPrefix(community.PDSURL, "https://") { 481 - log.Printf("Warning: Skipping non-HTTPS PDS URL for community %s", community.DID) 482 - } else if !strings.HasPrefix(community.AvatarCID, "baf") { 483 - // Validate CID format (IPFS CIDs start with "baf" for CIDv1 base32) 484 - log.Printf("Warning: Invalid CID format for community %s", community.DID) 485 - } else { 486 - // Use proper URL escaping to prevent injection attacks 487 - avatarURLString := fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", 488 - strings.TrimSuffix(community.PDSURL, "/"), 489 - url.QueryEscape(community.DID), 490 - url.QueryEscape(community.AvatarCID)) 491 - avatarURL = &avatarURLString 492 - } 493 - } 494 } else { 495 - // Log warning but don't fail the entire request 496 - log.Printf("Warning: Failed to fetch community for post %s: %v", post.CommunityDID, err) 497 } 498 499 communityRef := &posts.CommunityRef{ 500 DID: post.CommunityDID, 501 Name: communityName, 502 Avatar: avatarURL, 503 }
··· 458 } 459 460 // Build community reference - fetch community to get name and avatar (required by lexicon) 461 + // The lexicon marks communityRef.name and handle as required, so DIDs alone are insufficient 462 + // DATA INTEGRITY: Community should always exist for posts. If missing, it indicates orphaned data. 463 + community, err := s.communityRepo.GetByDID(ctx, post.CommunityDID) 464 + if err != nil { 465 + // This indicates a data integrity issue: post references non-existent community 466 + // Log as ERROR (not warning) since this should never happen in normal operation 467 + log.Printf("ERROR: Data integrity issue - post %s references non-existent community %s: %v", 468 + post.URI, post.CommunityDID, err) 469 + // Use DID as fallback for both handle and name to prevent breaking the API 470 + // This allows the response to be returned while surfacing the integrity issue in logs 471 + community = &communities.Community{ 472 + DID: post.CommunityDID, 473 + Handle: post.CommunityDID, // Fallback: use DID as handle 474 + Name: post.CommunityDID, // Fallback: use DID as name 475 } 476 + } 477 478 + // Capture handle for communityRef (required by lexicon) 479 + communityHandle := community.Handle 480 + 481 + // Determine display name: prefer DisplayName, fall back to Name, then Handle 482 + var communityName string 483 + if community.DisplayName != "" { 484 + communityName = community.DisplayName 485 + } else if community.Name != "" { 486 + communityName = community.Name 487 } else { 488 + communityName = community.Handle 489 + } 490 + 491 + // Build avatar URL from CID if available 492 + // Avatar is stored as blob in community's repository 493 + // Format: https://{pds}/xrpc/com.atproto.sync.getBlob?did={community_did}&cid={avatar_cid} 494 + var avatarURL *string 495 + if community.AvatarCID != "" && community.PDSURL != "" { 496 + // Validate HTTPS for security (prevent mixed content warnings, MitM attacks) 497 + if !strings.HasPrefix(community.PDSURL, "https://") { 498 + log.Printf("Warning: Skipping non-HTTPS PDS URL for community %s", community.DID) 499 + } else if !strings.HasPrefix(community.AvatarCID, "baf") { 500 + // Validate CID format (IPFS CIDs start with "baf" for CIDv1 base32) 501 + log.Printf("Warning: Invalid CID format for community %s", community.DID) 502 + } else { 503 + // Use proper URL escaping to prevent injection attacks 504 + avatarURLString := fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", 505 + strings.TrimSuffix(community.PDSURL, "/"), 506 + url.QueryEscape(community.DID), 507 + url.QueryEscape(community.AvatarCID)) 508 + avatarURL = &avatarURLString 509 + } 510 } 511 512 communityRef := &posts.CommunityRef{ 513 DID: post.CommunityDID, 514 + Handle: communityHandle, 515 Name: communityName, 516 Avatar: avatarURL, 517 }