Adds a Sakura Checker button to Amazon.co.jp product pages. Automatically detects suspicious reviews and changes the button color based on risk level.
// ==UserScript==
// @name Sakura Checker Button / サクラチェッカーで確認
// @name:ja サクラチェッカーで確認 + サクラ判定くん
// @namespace https://greasyfork.org/
// @match https://www.amazon.co.jp/dp/*
// @match https://www.amazon.co.jp/*/dp/*
// @match https://www.amazon.co.jp/gp/product/*
// @version 2.2.0
// @description Adds a Sakura Checker button to Amazon.co.jp product pages. Automatically detects suspicious reviews and changes the button color based on risk level.
// @description:ja Amazon.co.jpの商品ページにサクラチェッカーボタンを追加します。怪しいレビューを自動判定しリスクに応じてボタンの色が変わります。
// @icon https://www.amazon.co.jp/favicon.ico
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect www.amazon.co.jp
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const CACHE_EXPIRE = 1000 * 60 * 60 * 24;
const saveCache = (asin, data) => GM_setValue(`cache_${asin}`, JSON.stringify({ data, timestamp: Date.now() }));
const loadCache = (asin) => {
try {
const raw = GM_getValue(`cache_${asin}`);
if (!raw) return null;
const { data, timestamp } = JSON.parse(raw);
if (Date.now() - timestamp > CACHE_EXPIRE) return null;
return data;
} catch(e) { return null; }
};
const getASIN = () => {
const m = location.pathname.match(/\/([A-Z0-9]{10})(?:[/?]|$)/);
if (m) return m[1];
return document.querySelector('[name="ASIN"]')?.value || '';
};
const getTarget = () => {
const SELECTORS = [
'#buyNow', '#add-to-cart-button', '#buybox .a-button-stack',
'#add-to-cart-button-ubb', '#buybox-see-all-buying-choices',
'#dealsAccordionRow', '#outOfStock'
];
for (const sel of SELECTORS) {
const el = document.querySelector(sel);
if (el) return el.closest('div.a-section') || el.parentElement;
}
return null;
};
const getStarDistribution = () => {
const result = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 };
document.querySelectorAll('tr.a-histogram-row').forEach(row => {
const starEl = row.querySelector('.a-text-right');
const pctEl = row.querySelector('.a-text-left .a-size-base');
if (starEl && pctEl) {
const star = parseInt(starEl.textContent);
const pct = parseInt(pctEl.textContent);
if (!isNaN(star) && !isNaN(pct)) result[star] = pct;
}
});
return result;
};
const getTitle = () => document.querySelector('#productTitle')?.textContent?.trim() || '';
const getPrice = () => {
const selectors = [
'#corePriceDisplay_desktop_feature_div .a-price:first-of-type .a-offscreen',
'#apex_offerDisplay_desktop .a-price .a-offscreen',
'#priceblock_ourprice', '#priceblock_dealprice',
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) {
const text = el.textContent.trim();
if (!text.includes('¥') && !text.includes('¥')) continue;
const price = parseInt(text.replace(/[^0-9]/g, ''));
if (!isNaN(price) && price > 0 && price < 100000000) return price;
}
}
return null;
};
const getSellerInfo = () => {
const info = { sellerName: '', isAmazon: false, sellerId: '' };
const sellerEl = document.querySelector('#sellerProfileTriggerId, #merchant-info a');
if (sellerEl) {
info.sellerName = sellerEl.textContent.trim();
info.isAmazon = info.sellerName.includes('Amazon');
const href = sellerEl.getAttribute('href') || '';
const m = href.match(/seller=([A-Z0-9]+)/);
if (m) info.sellerId = m[1];
}
const soldBy = document.querySelector('#merchant-info')?.textContent || '';
if (soldBy.includes('Amazon.co.jp')) info.isAmazon = true;
return info;
};
const quickSakuraScore = () => {
const stars = getStarDistribution();
const title = getTitle();
const seller = getSellerInfo();
const price = getPrice();
let score = 0;
const s5 = stars[5], s1 = stars[1];
if (s5 >= 80) score += 40;
else if (s5 >= 70) score += 30;
else if (s5 >= 60) score += 20;
if (s5 >= 50 && s1 >= 20) score += 35;
if (title.length > 100) score += 15;
if (!seller.isAmazon && seller.sellerName) score += 10;
if (price && price < 2000) score += 20;
else if (price && price < 5000) score += 10;
return Math.min(score, 100);
};
const getButtonStyle = (score) => {
if (score >= 60) return { bg: 'rgb(120, 20, 20)', border: 'rgba(255, 80, 80, 0.7)', glow: 'rgba(255, 80, 80, 0.3)', emoji: '🚨', scoreColor: '#ff8080' };
if (score >= 30) return { bg: 'rgb(100, 70, 10)', border: 'rgba(255, 180, 0, 0.7)', glow: 'rgba(255, 180, 0, 0.3)', emoji: '⚠️', scoreColor: '#ffd080' };
return { bg: 'rgb(25, 26, 44)', border: 'rgba(233, 30, 140, 0.5)', glow: 'rgba(233, 30, 140, 0.2)', emoji: '✅', scoreColor: '#80ff80' };
};
// カートボタンのスタイルをそのままコピー
const cloneCartButtonStyle = () => {
const cartBtn = document.querySelector('#add-to-cart-button');
if (!cartBtn) return { width: '100%', height: '44px', borderRadius: '20px', fontSize: '14px' };
const computed = window.getComputedStyle(cartBtn);
return {
width: cartBtn.offsetWidth + 'px',
height: cartBtn.offsetHeight + 'px',
borderRadius: computed.borderRadius,
fontSize: computed.fontSize,
};
};
const addButton = () => {
if (document.getElementById('sakura-btn')) return;
const asin = getASIN();
if (!asin) return;
const target = getTarget();
if (!target) return;
const score = quickSakuraScore();
const style = getButtonStyle(score);
const cartStyle = cloneCartButtonStyle();
const btn = document.createElement('a');
btn.id = 'sakura-btn';
btn.href = `https://sakura-checker.jp/search/${asin}/`;
btn.target = '_self';
btn.innerHTML = `
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
<span style="font-size:18px;line-height:1;">🌸</span>
<span style="font-weight:bold;">Sakura Check</span>
<span id="sakura-score-badge" style="
background:rgba(0,0,0,0.25);
border-radius:20px;
padding:2px 8px;
font-size:12px;
font-weight:bold;
color:${style.scoreColor};
">${style.emoji} ${score}</span>
</div>
`;
btn.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
margin: 6px 0;
width: ${cartStyle.width};
height: ${cartStyle.height};
box-sizing: border-box;
background: ${style.bg};
color: #fff;
border-radius: ${cartStyle.borderRadius};
text-decoration: none;
font-family: sans-serif;
font-size: ${cartStyle.fontSize};
border: 1px solid ${style.border};
box-shadow: 0 0 12px ${style.glow};
transition: box-shadow 0.2s, transform 0.2s;
cursor: pointer;
`;
btn.addEventListener('mouseenter', () => {
btn.style.boxShadow = `0 0 20px ${style.glow.replace('0.3', '0.6')}`;
btn.style.transform = 'translateY(-1px)';
});
btn.addEventListener('mouseleave', () => {
btn.style.boxShadow = `0 0 12px ${style.glow}`;
btn.style.transform = 'translateY(0)';
});
target.after(btn);
analyzeAndUpdate(asin, btn);
};
const analyzeAndUpdate = (asin, btn) => {
const cached = loadCache(asin);
if (cached !== null) { updateButton(btn, cached); return; }
GM_xmlhttpRequest({
method: 'GET',
url: `https://www.amazon.co.jp/product-reviews/${asin}/?sortBy=recent&pageNumber=1`,
onload: (res) => {
try {
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
const dates = [];
doc.querySelectorAll('[data-hook="review-date"]').forEach(el => {
const m = el.textContent.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
if (m) dates.push(`${m[1]}-${m[2].padStart(2,'0')}-${m[3].padStart(2,'0')}`);
});
let dateScore = 0;
if (dates.length > 0) {
const dateCounts = {};
dates.forEach(d => { dateCounts[d] = (dateCounts[d] || 0) + 1; });
const maxCount = Math.max(...Object.values(dateCounts));
const rate = Math.round((maxCount / dates.length) * 100);
if (rate >= 50) dateScore = 35;
else if (rate >= 30) dateScore = 20;
}
const finalScore = Math.min(quickSakuraScore() + dateScore, 100);
saveCache(asin, finalScore);
updateButton(btn, finalScore);
} catch(e) {}
}
});
};
const updateButton = (btn, score) => {
const style = getButtonStyle(score);
btn.style.background = style.bg;
btn.style.borderColor = style.border;
btn.style.boxShadow = `0 0 12px ${style.glow}`;
const badge = document.getElementById('sakura-score-badge');
if (badge) { badge.textContent = `${style.emoji} ${score}`; badge.style.color = style.scoreColor; }
};
addButton();
const observer = new MutationObserver(() => addButton());
observer.observe(document.body, { childList: true, subtree: true });
})();