Fix reviews and trust score calculation, improve metadata API docs
- Fix reviews/recent and getRecentReviews to return all reviews (not just those with parsed ratings) - Fix trust score calculation to parse ratings from content text (e.g., [5/5]) - Fix getReviewSummary and getReviewStats to parse ratings from content - Fix OpenAPI schema for contact field (should be array, not object) - Fix popular/trending mints endpoints to show results even without pageviews
This commit is contained in:
@@ -1794,9 +1794,22 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au
|
|||||||
description: { type: 'string', nullable: true },
|
description: { type: 'string', nullable: true },
|
||||||
description_long: { type: 'string', nullable: true },
|
description_long: { type: 'string', nullable: true },
|
||||||
contact: {
|
contact: {
|
||||||
type: 'object',
|
type: 'array',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
additionalProperties: { type: 'string' }
|
description: 'Contact methods for the mint operator',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
method: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Contact method (email, twitter, nostr, etc.)'
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Contact identifier (email address, handle, npub, etc.)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
motd: {
|
motd: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|||||||
@@ -214,20 +214,34 @@ export function getReviews(mintId, options = {}) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get review stats for a mint
|
* Get review stats for a mint
|
||||||
|
* Parses ratings from content text if rating column is null
|
||||||
*/
|
*/
|
||||||
export function getReviewStats(mintId) {
|
export function getReviewStats(mintId) {
|
||||||
return queryOne(`
|
return queryOne(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_reviews,
|
COUNT(*) as total_reviews,
|
||||||
AVG(rating) as average_rating,
|
AVG(parsed_rating) as average_rating,
|
||||||
COUNT(DISTINCT pubkey) as unique_reviewers,
|
COUNT(DISTINCT pubkey) as unique_reviewers,
|
||||||
SUM(CASE WHEN rating = 5 THEN 1 ELSE 0 END) as five_star,
|
SUM(CASE WHEN parsed_rating = 5 THEN 1 ELSE 0 END) as five_star,
|
||||||
SUM(CASE WHEN rating = 4 THEN 1 ELSE 0 END) as four_star,
|
SUM(CASE WHEN parsed_rating = 4 THEN 1 ELSE 0 END) as four_star,
|
||||||
SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) as three_star,
|
SUM(CASE WHEN parsed_rating = 3 THEN 1 ELSE 0 END) as three_star,
|
||||||
SUM(CASE WHEN rating = 2 THEN 1 ELSE 0 END) as two_star,
|
SUM(CASE WHEN parsed_rating = 2 THEN 1 ELSE 0 END) as two_star,
|
||||||
SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) as one_star
|
SUM(CASE WHEN parsed_rating = 1 THEN 1 ELSE 0 END) as one_star
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
pubkey,
|
||||||
|
CASE
|
||||||
|
WHEN rating IS NOT NULL THEN rating
|
||||||
|
WHEN content LIKE '[5/5]%' OR content LIKE '%(5/5)%' THEN 5
|
||||||
|
WHEN content LIKE '[4/5]%' OR content LIKE '%(4/5)%' THEN 4
|
||||||
|
WHEN content LIKE '[3/5]%' OR content LIKE '%(3/5)%' THEN 3
|
||||||
|
WHEN content LIKE '[2/5]%' OR content LIKE '%(2/5)%' THEN 2
|
||||||
|
WHEN content LIKE '[1/5]%' OR content LIKE '%(1/5)%' THEN 1
|
||||||
|
ELSE NULL
|
||||||
|
END as parsed_rating
|
||||||
FROM reviews
|
FROM reviews
|
||||||
WHERE mint_id = ? AND rating IS NOT NULL
|
WHERE mint_id = ?
|
||||||
|
) parsed
|
||||||
`, [mintId]);
|
`, [mintId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,24 +395,20 @@ export function countReviews(options = {}) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get review summary for a mint (quick overview)
|
* Get review summary for a mint (quick overview)
|
||||||
|
* Parses ratings from content text if rating column is null
|
||||||
*/
|
*/
|
||||||
export function getReviewSummary(mintId) {
|
export function getReviewSummary(mintId) {
|
||||||
const stats = queryOne(`
|
// First get total count of all reviews
|
||||||
|
const totalStats = queryOne(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_reviews,
|
COUNT(*) as total_reviews,
|
||||||
AVG(rating) as avg_rating,
|
|
||||||
COUNT(DISTINCT pubkey) as unique_reviewers,
|
COUNT(DISTINCT pubkey) as unique_reviewers,
|
||||||
SUM(CASE WHEN rating = 5 THEN 1 ELSE 0 END) as five_star,
|
|
||||||
SUM(CASE WHEN rating = 4 THEN 1 ELSE 0 END) as four_star,
|
|
||||||
SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) as three_star,
|
|
||||||
SUM(CASE WHEN rating = 2 THEN 1 ELSE 0 END) as two_star,
|
|
||||||
SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) as one_star,
|
|
||||||
MAX(created_at) as last_review_at
|
MAX(created_at) as last_review_at
|
||||||
FROM reviews
|
FROM reviews
|
||||||
WHERE mint_id = ? AND rating IS NOT NULL
|
WHERE mint_id = ?
|
||||||
`, [mintId]);
|
`, [mintId]);
|
||||||
|
|
||||||
if (!stats || stats.total_reviews === 0) {
|
if (!totalStats || totalStats.total_reviews === 0) {
|
||||||
return {
|
return {
|
||||||
total_reviews: 0,
|
total_reviews: 0,
|
||||||
avg_rating: null,
|
avg_rating: null,
|
||||||
@@ -407,18 +417,43 @@ export function getReviewSummary(mintId) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse ratings from content if rating column is null
|
||||||
|
const ratingStats = queryOne(`
|
||||||
|
SELECT
|
||||||
|
AVG(parsed_rating) as avg_rating,
|
||||||
|
SUM(CASE WHEN parsed_rating = 5 THEN 1 ELSE 0 END) as five_star,
|
||||||
|
SUM(CASE WHEN parsed_rating = 4 THEN 1 ELSE 0 END) as four_star,
|
||||||
|
SUM(CASE WHEN parsed_rating = 3 THEN 1 ELSE 0 END) as three_star,
|
||||||
|
SUM(CASE WHEN parsed_rating = 2 THEN 1 ELSE 0 END) as two_star,
|
||||||
|
SUM(CASE WHEN parsed_rating = 1 THEN 1 ELSE 0 END) as one_star
|
||||||
|
FROM (
|
||||||
|
SELECT CASE
|
||||||
|
WHEN rating IS NOT NULL THEN rating
|
||||||
|
WHEN content LIKE '[5/5]%' OR content LIKE '%(5/5)%' THEN 5
|
||||||
|
WHEN content LIKE '[4/5]%' OR content LIKE '%(4/5)%' THEN 4
|
||||||
|
WHEN content LIKE '[3/5]%' OR content LIKE '%(3/5)%' THEN 3
|
||||||
|
WHEN content LIKE '[2/5]%' OR content LIKE '%(2/5)%' THEN 2
|
||||||
|
WHEN content LIKE '[1/5]%' OR content LIKE '%(1/5)%' THEN 1
|
||||||
|
ELSE NULL
|
||||||
|
END as parsed_rating
|
||||||
|
FROM reviews
|
||||||
|
WHERE mint_id = ?
|
||||||
|
) parsed
|
||||||
|
WHERE parsed_rating IS NOT NULL
|
||||||
|
`, [mintId]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total_reviews: stats.total_reviews,
|
total_reviews: totalStats.total_reviews,
|
||||||
avg_rating: stats.avg_rating ? Math.round(stats.avg_rating * 10) / 10 : null,
|
avg_rating: ratingStats && ratingStats.avg_rating ? Math.round(ratingStats.avg_rating * 10) / 10 : null,
|
||||||
unique_reviewers: stats.unique_reviewers,
|
unique_reviewers: totalStats.unique_reviewers,
|
||||||
rating_distribution: {
|
rating_distribution: {
|
||||||
5: stats.five_star || 0,
|
5: (ratingStats && ratingStats.five_star) || 0,
|
||||||
4: stats.four_star || 0,
|
4: (ratingStats && ratingStats.four_star) || 0,
|
||||||
3: stats.three_star || 0,
|
3: (ratingStats && ratingStats.three_star) || 0,
|
||||||
2: stats.two_star || 0,
|
2: (ratingStats && ratingStats.two_star) || 0,
|
||||||
1: stats.one_star || 0
|
1: (ratingStats && ratingStats.one_star) || 0
|
||||||
},
|
},
|
||||||
last_review_at: stats.last_review_at ? unixToISO(stats.last_review_at) : null
|
last_review_at: totalStats.last_review_at ? unixToISO(totalStats.last_review_at) : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,14 +142,23 @@ function calculateSpeedScore(mintId, component) {
|
|||||||
* Calculate reviews score (max 20 points)
|
* Calculate reviews score (max 20 points)
|
||||||
*/
|
*/
|
||||||
function calculateReviewsScore(mintId, component) {
|
function calculateReviewsScore(mintId, component) {
|
||||||
// Get review stats
|
// Get review stats - include all reviews, not just those with parsed ratings
|
||||||
|
// Parse rating from content if rating column is null (ratings often in "[5/5]" format in content)
|
||||||
const stats = queryOne(`
|
const stats = queryOne(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as count,
|
COUNT(*) as count,
|
||||||
AVG(rating) as avg_rating,
|
AVG(CASE
|
||||||
|
WHEN rating IS NOT NULL THEN rating
|
||||||
|
WHEN content LIKE '[5/5]%' OR content LIKE '%(5/5)%' THEN 5
|
||||||
|
WHEN content LIKE '[4/5]%' OR content LIKE '%(4/5)%' THEN 4
|
||||||
|
WHEN content LIKE '[3/5]%' OR content LIKE '%(3/5)%' THEN 3
|
||||||
|
WHEN content LIKE '[2/5]%' OR content LIKE '%(2/5)%' THEN 2
|
||||||
|
WHEN content LIKE '[1/5]%' OR content LIKE '%(1/5)%' THEN 1
|
||||||
|
ELSE NULL
|
||||||
|
END) as avg_rating,
|
||||||
COUNT(DISTINCT pubkey) as unique_reviewers
|
COUNT(DISTINCT pubkey) as unique_reviewers
|
||||||
FROM reviews
|
FROM reviews
|
||||||
WHERE mint_id = ? AND rating IS NOT NULL
|
WHERE mint_id = ?
|
||||||
`, [mintId]);
|
`, [mintId]);
|
||||||
|
|
||||||
if (!stats || stats.count === 0) {
|
if (!stats || stats.count === 0) {
|
||||||
@@ -163,10 +172,12 @@ function calculateReviewsScore(mintId, component) {
|
|||||||
|
|
||||||
component.details = {
|
component.details = {
|
||||||
review_count: reviewCount,
|
review_count: reviewCount,
|
||||||
average_rating: Math.round(avgRating * 10) / 10,
|
average_rating: avgRating ? Math.round(avgRating * 10) / 10 : null,
|
||||||
unique_reviewers: uniqueReviewers
|
unique_reviewers: uniqueReviewers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If we have a calculated average rating, use it
|
||||||
|
if (avgRating) {
|
||||||
// Base score from average rating (1-5 scale)
|
// Base score from average rating (1-5 scale)
|
||||||
const ratingScore = ((avgRating - 1) / 4) * (trustWeights.reviews * 0.7);
|
const ratingScore = ((avgRating - 1) / 4) * (trustWeights.reviews * 0.7);
|
||||||
|
|
||||||
@@ -179,6 +190,16 @@ function calculateReviewsScore(mintId, component) {
|
|||||||
return ratingScore + quantityBonus;
|
return ratingScore + quantityBonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: if we have reviews but no parseable ratings, give partial credit based on count
|
||||||
|
// This rewards having reviews even without explicit ratings
|
||||||
|
const quantityScore = Math.min(
|
||||||
|
trustWeights.reviews * 0.5, // Max 50% of review score without ratings
|
||||||
|
(Math.log10(reviewCount + 1) / 2) * trustWeights.reviews * 0.5
|
||||||
|
);
|
||||||
|
|
||||||
|
return quantityScore;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate identity score (max 10 points)
|
* Calculate identity score (max 10 points)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user