Aimpress_site/server/sync-reviews.mjs
Vadym Samoilenko 37f250f7ed Filter out reviews without text, sync twice a month
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:40:13 +00:00

147 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Google Reviews Sync Script
*
* Fetches reviews from Google Places API (New) and saves them as reviews.json.
* Intended to run as a daily cron job on the server.
*
* Environment:
* GOOGLE_API_KEY — Google API key with Places API enabled
* PLACE_ID — Google Place ID for the business
* OUTPUT_PATH — Where to write reviews.json (defaults to /opt/00-infrastructure/Website/website/dist/reviews.json)
*
* Usage:
* GOOGLE_API_KEY=xxx PLACE_ID=yyy node ~/sync-reviews.mjs
*
* Cron (daily at 6am):
* 0 6 * * * GOOGLE_API_KEY=AIzaSyByehddoo7TNSyV6qjmyw2vuMmizIlvqzM PLACE_ID=ChIJ0Xh19SlCkggRGO-WYbMbPgY node ~/sync-reviews.mjs >> /var/log/sync-reviews.log 2>&1
*/
import fs from 'fs';
import path from 'path';
const API_KEY = process.env.GOOGLE_API_KEY || 'AIzaSyByehddoo7TNSyV6qjmyw2vuMmizIlvqzM';
const PLACE_ID = process.env.PLACE_ID || 'ChIJ0Xh19SlCkggRGO-WYbMbPgY';
const OUTPUT_PATH = process.env.OUTPUT_PATH || '/opt/00-infrastructure/Website/website/dist/reviews.json';
async function fetchReviews() {
// Use Places API (New) — places.googleapis.com
const url = `https://places.googleapis.com/v1/places/${PLACE_ID}?fields=reviews&key=${API_KEY}&languageCode=en`;
const res = await fetch(url, {
headers: {
'X-Goog-FieldMask': 'reviews',
},
});
if (!res.ok) {
// Fallback to legacy Places API
console.log('New API failed, trying legacy Places API...');
return fetchReviewsLegacy();
}
const data = await res.json();
return (data.reviews || []).map((r) => ({
author: r.authorAttribution?.displayName || 'Anonymous',
rating: r.rating || 5,
text: r.text?.text || '',
originalText: r.originalText?.text || '',
originalLanguage: r.originalText?.languageCode || 'en',
date: r.relativePublishTimeDescription || '',
profilePhoto: r.authorAttribution?.photoUri || '',
}));
}
async function fetchReviewsLegacy() {
const url = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${PLACE_ID}&fields=reviews&key=${API_KEY}&reviews_sort=newest`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Legacy Places API error: ${res.status} ${res.statusText}`);
}
const data = await res.json();
if (data.status !== 'OK') {
throw new Error(`Places API status: ${data.status}${data.error_message || ''}`);
}
return (data.result?.reviews || []).map((r) => ({
author: r.author_name || 'Anonymous',
rating: r.rating || 5,
text: r.text || '',
originalText: r.original_language !== 'en' ? r.text : '',
originalLanguage: r.language || 'en',
date: r.relative_time_description || '',
profilePhoto: r.profile_photo_url || '',
}));
}
async function translateIfNeeded(reviews) {
// Google Translate via free translate endpoint
const toTranslate = reviews.filter(
(r) => r.originalLanguage && r.originalLanguage !== 'en' && r.text
);
if (toTranslate.length === 0) return reviews;
for (const review of toTranslate) {
try {
const url = `https://translation.googleapis.com/language/translate/v2?key=${API_KEY}`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
q: review.text,
source: review.originalLanguage,
target: 'en',
format: 'text',
}),
});
if (res.ok) {
const data = await res.json();
const translated = data.data?.translations?.[0]?.translatedText;
if (translated) {
review.originalText = review.text;
review.text = translated;
}
}
} catch {
// Keep original text if translation fails
}
}
return reviews;
}
async function main() {
console.log(`[${new Date().toISOString()}] Syncing Google reviews...`);
console.log(`Place ID: ${PLACE_ID}`);
console.log(`Output: ${OUTPUT_PATH}`);
try {
let reviews = await fetchReviews();
console.log(`Fetched ${reviews.length} reviews`);
// Filter out reviews without text
reviews = reviews.filter((r) => r.text && r.text.trim().length > 0);
console.log(`${reviews.length} reviews have text`);
reviews = await translateIfNeeded(reviews);
// Ensure output directory exists
const dir = path.dirname(OUTPUT_PATH);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(reviews, null, 2));
console.log(`Saved ${reviews.length} reviews to ${OUTPUT_PATH}`);
} catch (err) {
console.error('Error syncing reviews:', err.message);
process.exit(1);
}
}
main();