Align extension search with public API and simplify extension controller
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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), " ,.;:-").'…';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user