147 lines
4.7 KiB
JavaScript
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();
|