Align extension search with public API and simplify extension controller

This commit is contained in:
Dwindi Ramadhana
2026-02-17 01:01:41 +07:00
parent 2726b6c312
commit 18f72fe61e
5 changed files with 7 additions and 181 deletions

View File

@@ -75,6 +75,7 @@ DEWEMOJI_PRO_MAX_LIMIT=50
DEWEMOJI_BILLING_MODE=sandbox
DEWEMOJI_CHECKOUT_PENDING_COOLDOWN=120
DEWEMOJI_ALLOWED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,https://dewemoji.com,https://www.dewemoji.com
DEWEMOJI_CORS_ALLOW_ALL_PUBLIC=true
DEWEMOJI_FRONTEND_HEADER=web-v1
DEWEMOJI_METRICS_ENABLED=true
DEWEMOJI_METRICS_TOKEN=

View File

@@ -606,6 +606,8 @@ class EmojiApiController extends Controller
$allowedOrigins = config('dewemoji.cors.allowed_origins', []);
if (is_array($allowedOrigins) && in_array($origin, $allowedOrigins, true)) {
$headers['Access-Control-Allow-Origin'] = $origin;
} elseif ((bool) config('dewemoji.cors.allow_all_public', true)) {
$headers['Access-Control-Allow-Origin'] = '*';
}
return response()->json($payload, $status, $headers + $extraHeaders);

View File

@@ -6,13 +6,9 @@ use App\Http\Controllers\Controller;
use App\Services\Extension\ExtensionVerificationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use RuntimeException;
class ExtensionController extends Controller
{
/** @var array<string,mixed>|null */
private static ?array $dataset = null;
public function __construct(private readonly ExtensionVerificationService $verifier)
{
}
@@ -28,180 +24,4 @@ class ExtensionController extends Controller
'verified' => $ok,
], $ok ? 200 : 403);
}
public function search(Request $request): JsonResponse
{
$token = trim((string) $request->header('X-Extension-Token', ''));
$expected = config('dewemoji.public_access.extension_ids', []);
$ok = $this->verifier->verifyToken($token, is_array($expected) ? $expected : []);
if (!$ok) {
return response()->json(['ok' => false, 'error' => 'extension_unverified'], 403);
}
try {
$data = $this->loadData();
} catch (RuntimeException $e) {
return response()->json([
'ok' => false,
'error' => 'data_load_failed',
'message' => $e->getMessage(),
], 500);
}
$items = $data['emojis'] ?? [];
$q = trim((string) ($request->query('q', $request->query('query', ''))));
$category = $this->normalizeCategoryFilter((string) $request->query('category', ''));
$subSlug = $this->slugify((string) $request->query('subcategory', ''));
$page = max((int) $request->query('page', 1), 1);
$defaultLimit = max((int) config('dewemoji.pagination.default_limit', 20), 1);
$maxLimit = max((int) config('dewemoji.pagination.free_max_limit', 20), 1);
$limit = min(max((int) $request->query('limit', $defaultLimit), 1), $maxLimit);
$filtered = $this->filterItems($items, $q, $category, $subSlug);
$total = count($filtered);
$offset = ($page - 1) * $limit;
$pageItems = array_slice($filtered, $offset, $limit);
return response()->json([
'ok' => true,
'items' => array_map(fn (array $item): array => $this->transformItem($item), $pageItems),
'total' => $total,
'page' => $page,
'limit' => $limit,
'plan' => 'free',
]);
}
/**
* @return array<string,mixed>
*/
private function loadData(): array
{
if (self::$dataset !== null) {
return self::$dataset;
}
$path = (string) config('dewemoji.data_path');
if (!is_file($path)) {
throw new RuntimeException('Emoji dataset file was not found at: '.$path);
}
$raw = file_get_contents($path);
if ($raw === false) {
throw new RuntimeException('Emoji dataset file could not be read.');
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
throw new RuntimeException('Emoji dataset JSON is invalid.');
}
self::$dataset = $decoded;
return self::$dataset;
}
private function normalizeCategoryFilter(string $category): string
{
$value = strtolower(trim($category));
if ($value === '' || $value === 'all') {
return '';
}
$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',
];
return $map[$value] ?? $category;
}
private function slugify(string $text): string
{
$value = strtolower(trim($text));
$value = str_replace('&', 'and', $value);
$value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? '';
return trim($value, '-');
}
/**
* @param array<int,array<string,mixed>> $items
* @return array<int,array<string,mixed>>
*/
private function filterItems(array $items, string $q, string $category, string $subSlug): array
{
return array_values(array_filter($items, function (array $item) use ($q, $category, $subSlug): bool {
$itemCategory = trim((string) ($item['category'] ?? ''));
$itemSubcategory = trim((string) ($item['subcategory'] ?? ''));
if ($category !== '' && strcasecmp($itemCategory, $category) !== 0) {
return false;
}
if ($subSlug !== '' && $this->slugify($itemSubcategory) !== $subSlug) {
return false;
}
if ($q === '') {
return true;
}
$haystack = strtolower(implode(' ', [
(string) ($item['emoji'] ?? ''),
(string) ($item['name'] ?? ''),
(string) ($item['slug'] ?? ''),
$itemCategory,
$itemSubcategory,
implode(' ', $item['keywords_en'] ?? []),
implode(' ', $item['keywords_id'] ?? []),
implode(' ', $item['aliases'] ?? []),
implode(' ', $item['shortcodes'] ?? []),
implode(' ', $item['alt_shortcodes'] ?? []),
implode(' ', $item['intent_tags'] ?? []),
]));
$tokens = preg_split('/\s+/', strtolower($q)) ?: [];
foreach ($tokens as $token) {
if ($token === '') {
continue;
}
if (!str_contains($haystack, $token)) {
return false;
}
}
return true;
}));
}
/**
* @param array<string,mixed> $item
* @return array<string,mixed>
*/
private function transformItem(array $item): array
{
return [
'emoji' => (string) ($item['emoji'] ?? ''),
'name' => (string) ($item['name'] ?? ''),
'slug' => (string) ($item['slug'] ?? ''),
'category' => (string) ($item['category'] ?? ''),
'subcategory' => (string) ($item['subcategory'] ?? ''),
'supports_skin_tone' => (bool) ($item['supports_skin_tone'] ?? false),
'summary' => $this->summary((string) ($item['description'] ?? ''), 150),
];
}
private function summary(string $text, int $max): string
{
$text = trim(preg_replace('/\s+/', ' ', strip_tags($text)) ?? '');
if (mb_strlen($text) <= $max) {
return $text;
}
return rtrim(mb_substr($text, 0, $max - 1), " ,.;:-").'…';
}
}

View File

@@ -56,6 +56,7 @@ return [
'cors' => [
'allowed_origins' => array_values(array_filter(array_map('trim', explode(',', (string) env('DEWEMOJI_ALLOWED_ORIGINS', 'http://127.0.0.1:8000,http://localhost:8000,https://dewemoji.com,https://www.dewemoji.com'))))),
'allow_all_public' => filter_var(env('DEWEMOJI_CORS_ALLOW_ALL_PUBLIC', true), FILTER_VALIDATE_BOOL),
'allow_methods' => 'GET, POST, OPTIONS',
'allow_headers' => 'Content-Type, Authorization, X-Api-Key, X-Admin-Token, X-Account-Id, X-Dewemoji-Frontend, X-Extension-Token',
],

View File

@@ -26,6 +26,8 @@ Route::options('/v1/{any}', function () {
$allowedOrigins = config('dewemoji.cors.allowed_origins', []);
if (is_array($allowedOrigins) && in_array($origin, $allowedOrigins, true)) {
$headers['Access-Control-Allow-Origin'] = $origin;
} elseif ((bool) config('dewemoji.cors.allow_all_public', true)) {
$headers['Access-Control-Allow-Origin'] = '*';
}
return response('', 204, $headers);
@@ -42,7 +44,7 @@ Route::prefix('v1')->group(function () {
Route::get('/emoji/{slug}', [EmojiApiController::class, 'emoji']);
Route::get('/pricing', [PricingController::class, 'index']);
Route::post('/extension/verify', [ExtensionController::class, 'verify']);
Route::get('/extension/search', [ExtensionController::class, 'search']);
Route::get('/extension/search', [EmojiApiController::class, 'emojis']);
Route::post('/user/register', [UserController::class, 'register']);
Route::post('/user/login', [UserController::class, 'login']);