package creator import ( "context" "fmt" "time" ) // CommentAggregationFilter uses comment publication time, not collection time. type CommentAggregationFilter struct { SourceType string SourceID string Hours int } func (filter CommentAggregationFilter) Validate() error { if filter.SourceType != SourceOwned && filter.SourceType != SourceCompetitor { return ErrInvalid } switch filter.Hours { case 1, 6, 12, 24, 72, 120, 168: return nil default: return ErrInvalid } } // AggregatedComment adds the owning account and work to an already collected comment. type AggregatedComment struct { ID string `json:"id"` AuthorUID string `json:"author_uid"` AuthorName string `json:"author_name"` Content string `json:"content"` PublishedAt time.Time `json:"published_at"` SourceType string `json:"source_type"` SourceID string `json:"source_id"` SourceName string `json:"source_name"` SourceAccountKey string `json:"source_account_key"` WorkID string `json:"work_id"` WorkKey string `json:"work_key"` WorkTitle string `json:"work_title"` WorkURL string `json:"work_url"` } func buildCommentAggregationQuery(filter CommentAggregationFilter, now time.Time) (string, []any, error) { if err := filter.Validate(); err != nil { return "", nil, err } query := ` FROM creator_comment c JOIN creator_work w ON w.id = c.work_id LEFT JOIN social_account a ON w.source_type = 'owned' AND a.account_id = w.source_id LEFT JOIN creator_competitor rival ON w.source_type = 'competitor' AND rival.competitor_id = w.source_id WHERE w.source_type = $1 AND c.published_at >= $2 AND c.published_at <= $3` args := []any{filter.SourceType, now.UTC().Add(-time.Duration(filter.Hours) * time.Hour), now.UTC()} if filter.SourceID != "" { args = append(args, filter.SourceID) query += " AND w.source_id = $4" } return query, args, nil } func (s *Store) ListCommentAggregationPage(ctx context.Context, filter CommentAggregationFilter, page, pageSize int, now time.Time) (Page[AggregatedComment], error) { offset, limit, err := pageBounds(page, pageSize) if err != nil { return Page[AggregatedComment]{}, err } from, args, err := buildCommentAggregationQuery(filter, now) if err != nil { return Page[AggregatedComment]{}, err } result := Page[AggregatedComment]{Data: []AggregatedComment{}, Page: page, PageSize: pageSize} if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*)"+from, args...).Scan(&result.Total); err != nil { return Page[AggregatedComment]{}, fmt.Errorf("count comment aggregation source=%s account=%s hours=%d: %w", filter.SourceType, filter.SourceID, filter.Hours, err) } query := `SELECT c.comment_id, c.author_uid, c.author_name, c.content, c.published_at, w.source_type, w.source_id, CASE WHEN w.source_type = 'owned' THEN COALESCE(a.name, '') ELSE COALESCE(rival.nickname, '') END, CASE WHEN w.source_type = 'owned' THEN COALESCE(a.platform_account_key, '') ELSE COALESCE(rival.platform_account_key, '') END, w.work_id, w.work_key, w.title, w.original_url` + from query += fmt.Sprintf(" ORDER BY c.published_at DESC, c.id DESC LIMIT $%d OFFSET $%d", len(args)+1, len(args)+2) args = append(args, limit, offset) rows, err := s.db.QueryContext(ctx, query, args...) if err != nil { return Page[AggregatedComment]{}, fmt.Errorf("list comment aggregation source=%s account=%s hours=%d page=%d: %w", filter.SourceType, filter.SourceID, filter.Hours, page, err) } defer rows.Close() for rows.Next() { var item AggregatedComment if err := rows.Scan(&item.ID, &item.AuthorUID, &item.AuthorName, &item.Content, &item.PublishedAt, &item.SourceType, &item.SourceID, &item.SourceName, &item.SourceAccountKey, &item.WorkID, &item.WorkKey, &item.WorkTitle, &item.WorkURL); err != nil { return Page[AggregatedComment]{}, fmt.Errorf("scan comment aggregation: %w", err) } item.PublishedAt = item.PublishedAt.UTC() result.Data = append(result.Data, item) } if err := rows.Err(); err != nil { return Page[AggregatedComment]{}, fmt.Errorf("read comment aggregation: %w", err) } result.HasNext = offset+len(result.Data) < result.Total return result, nil }