Files
emoji-api/api/emojis.php
2025-08-29 23:47:11 +07:00

231 lines
7.9 KiB
PHP

<?php
// api/emojis.php (route: /api/emojis?q=...&category=...&subcategory=...&page=1&limit=50)
require_once __DIR__.'/../helpers/auth.php';
require_once __DIR__.'/../helpers/filters.php';
// ---- Skin tone helpers (Fitzpatrick modifiers) ----
if (!function_exists('dw_supports_skin_tone')) {
function dw_supports_skin_tone(array $emoji): bool {
// Respect explicit flag if present in source data
if (isset($emoji['supports_skin_tone'])) {
return (bool)$emoji['supports_skin_tone'];
}
$cat = strtolower(trim($emoji['category'] ?? ''));
$sub = strtolower(trim($emoji['subcategory'] ?? ''));
$name = strtolower(trim($emoji['name'] ?? ''));
// 1) category must be People & Body
if ($cat !== 'people & body') return false;
// 2) Exclude faces: many face emojis are not tone-modifiable
if (preg_match('~\bface\b~', $name) || preg_match('~\bface\b~', $sub)) return false;
// 3) Allowlist of tone-capable subcategories (based on Unicode charts)
$allowSubs = [
'hand-fingers-open','hand-fingers-partial','hand-fingers-closed','hand-single-finger',
'hands','hand-prop','handshake','body-parts','person','person-gesture','person-activity','person-resting'
];
if (in_array($sub, $allowSubs, true) || (function_exists('str_starts_with') ? str_starts_with($sub, 'hand') : strpos($sub, 'hand') === 0)) return true;
// Fallback by keywords mentioning body parts / gestures
$keys = [];
foreach (['keywords','keywords_en','keywords_id'] as $k) {
if (!empty($emoji[$k]) && is_array($emoji[$k])) $keys = array_merge($keys, $emoji[$k]);
}
$hay = strtolower(' '.implode(' ', $keys).' ');
return (bool)preg_match('~\b(hand|hands|wave|clap|thumb|finger|palm|point|ear|nose|leg|foot|person|man|woman|boy|girl|salute)\b~', $hay);
}
}
if (!function_exists('dw_skin_tone_modifiers')) {
function dw_skin_tone_modifiers(): array {
return ["\u{1F3FB}", "\u{1F3FC}", "\u{1F3FD}", "\u{1F3FE}", "\u{1F3FF}"]; // light .. dark
}
}
if (!function_exists('dw_strip_tone')) {
function dw_strip_tone(string $emoji): string {
return preg_replace('/\x{1F3FB}|\x{1F3FC}|\x{1F3FD}|\x{1F3FE}|\x{1F3FF}/u', '', $emoji);
}
}
if (!function_exists('dw_build_tone_variants')) {
function dw_build_tone_variants(string $emojiBase): array {
$mods = ["\u{1F3FB}", "\u{1F3FC}", "\u{1F3FD}", "\u{1F3FE}", "\u{1F3FF}"];
return array_map(fn($m) => $emojiBase.$m, $mods);
}
}
header('Content-Type: application/json; charset=utf-8');
$tier = detectTier();
// Allow client hint via header (extension may send X-Dewemoji-Plan: pro|free)
$hint = strtolower(trim($_SERVER['HTTP_X_DEWEMOJI_PLAN'] ?? ''));
// Optional license verify (server-side spot check)
$acct = $_SERVER['HTTP_X_ACCOUNT_ID'] ?? '';
$key = $_SERVER['HTTP_X_LICENSE_KEY'] ?? '';
if ($acct && $key) {
// probabilistic verify, e.g., 1 in 50 requests
if (mt_rand(1,50) === 1) {
$verify = @file_get_contents('https://emojilicense.dewe.pw/v1/license/verify', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode(['key'=>$key,'account_id'=>$acct]),
'timeout' => 3,
]
]));
if ($verify) {
$v = json_decode($verify,true);
if (!empty($v['ok']) && !empty($v['plan'])) {
$tier = strtolower($v['plan']);
$isPro = ($tier==='pro');
}
}
}
}
if ($hint === 'pro' || $hint === 'free') {
$tier = $hint;
}
$isPro = ($tier === 'pro');
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
$whitelist = ['emoji.dewe.pw'];
$isWhitelisted = in_array($host, $whitelist, true)
|| in_array(parse_url($origin, PHP_URL_HOST) ?: '', $whitelist, true);
// --- Free vs Pro gating ---
$maxLimit = $isPro ? 50 : 20;
$maxPages = $isPro ? 20 : 5;
if ($isWhitelisted) {
$maxLimit = 100;
$maxPages = 1000;
}
// --- Helpers for slug <-> label normalization ---
$CATEGORY_MAP = [
'all' => 'all',
'smileys' => 'Smileys & Emotion',
'people' => 'People & Body',
'animals' => 'Animals & Nature',
'food' => 'Food & Drink',
'travel' => 'Travel & Places',
'activities'=> 'Activities',
'objects' => 'Objects',
'symbols' => 'Symbols',
'flags' => 'Flags',
];
function dw_slugify($s){
$s = strtolower(trim((string)$s));
$s = str_replace('&', 'and', $s);
$s = preg_replace('/[^a-z0-9]+/','-',$s);
return trim($s,'-');
}
// Parse incoming filters
$qParam = trim((string)($_GET['q'] ?? ''));
$catParam = trim((string)($_GET['category'] ?? ''));
$subParam = trim((string)($_GET['subcategory'] ?? ''));
// Normalize category: accept either slug ("people") or label ("People & Body")
$catLabel = '';
if ($catParam !== '' && strtolower($catParam) !== 'all') {
$catLabel = $CATEGORY_MAP[strtolower($catParam)] ?? $catParam; // fallback to raw label
}
// Normalize subcategory to a slug for comparison
$subSlug = $subParam !== '' ? dw_slugify($subParam) : '';
// Expose normalized vars for filter closure
$q = $qParam;
$cat = $catLabel; // label form (or '')
$sub = $subSlug; // slug form (or '')
// Parse query params with caps
$limit = min(max((int)($_GET['limit'] ?? $maxLimit), 1), $maxLimit);
$page = max((int)($_GET['page'] ?? 1), 1);
if ($page > $maxPages) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'items' => [],
'page' => $page,
'limit' => $limit,
'total' => 0,
'plan' => $isPro ? 'pro' : 'free',
'message' => 'Page limit reached for your plan.'
], JSON_UNESCAPED_UNICODE);
exit;
}
// Load data
$data = json_decode(file_get_contents(__DIR__.'/../data/emojis.json'), true);
$items = $data['emojis'] ?? [];
// Server-side filtering (case-insensitive)
$items = array_values(array_filter($items, function($e) use ($q,$cat,$sub) {
// Category match: compare by slug to be robust
if ($cat !== '') {
$itemCatSlug = dw_slugify($e['category'] ?? '');
$wantCatSlug = dw_slugify($cat);
if ($itemCatSlug !== $wantCatSlug) return false;
}
// Subcategory match: normalize item subcategory to slug and compare
if ($sub !== '') {
$itemSubSlug = dw_slugify($e['subcategory'] ?? '');
if ($itemSubSlug !== $sub) return false;
}
// Text search across common fields
if ($q !== '') {
$hay = strtolower(join(' ', [
$e['emoji'] ?? '',
$e['name'] ?? '',
$e['slug'] ?? '',
$e['category'] ?? '',
$e['subcategory'] ?? '',
join(' ', $e['keywords_en'] ?? []),
join(' ', $e['keywords_id'] ?? []),
join(' ', $e['aliases'] ?? []),
join(' ', $e['shortcodes'] ?? []),
]));
foreach (preg_split('/\s+/', strtolower($q)) as $tok) {
if ($tok === '') continue;
if (strpos($hay, $tok) === false) return false;
}
}
return true;
}));
$total = count($items);
$offset = ($page - 1) * $limit;
$items = array_slice($items, $offset, $limit);
// Field filtering + augmentation
$items = array_map(function($raw) use ($tier, $isPro) {
$e = filterEmoji($raw, $tier, true);
// add tone support & modifiers for clients
$e['supports_skin_tone'] = dw_supports_skin_tone($raw) || dw_supports_skin_tone($e);
if (!isset($e['skin_tone_modifiers'])) {
$e['skin_tone_modifiers'] = dw_skin_tone_modifiers();
}
if ($e['supports_skin_tone'] && !empty($e['emoji'])) {
$base = dw_strip_tone($e['emoji']);
if ($base !== '') {
$e['emoji_base'] = $base;
if ($isPro) {
$e['variants'] = dw_build_tone_variants($base);
}
}
}
return $e;
}, $items);
// ETag for caching
$etag = '"'.sha1(json_encode([$page,$limit,$q,$cat,$sub,$tier,$total,$items])).'"';
header('ETag: '.$etag);
header('Cache-Control: public, max-age=300');
if (($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') === $etag) { http_response_code(304); exit; }
echo json_encode([
'items' => $items,
'page' => $page,
'limit' => $limit,
'total' => $total,
'plan' => $isPro ? 'pro' : 'free',
], JSON_UNESCAPED_UNICODE);