feat: phase 2 api parity endpoints for extension
This commit is contained in:
226
app/app/Http/Controllers/Api/V1/EmojiApiController.php
Normal file
226
app/app/Http/Controllers/Api/V1/EmojiApiController.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use RuntimeException;
|
||||
|
||||
class EmojiApiController extends Controller
|
||||
{
|
||||
private const TIER_FREE = 'free';
|
||||
private const TIER_PRO = 'pro';
|
||||
|
||||
/** @var array<string,mixed>|null */
|
||||
private static ?array $dataset = null;
|
||||
|
||||
public function categories(): JsonResponse
|
||||
{
|
||||
$tier = $this->detectTier(request());
|
||||
try {
|
||||
$data = $this->loadData();
|
||||
} catch (RuntimeException $e) {
|
||||
return $this->jsonWithTier([
|
||||
'error' => 'data_load_failed',
|
||||
'message' => $e->getMessage(),
|
||||
], $tier, 500);
|
||||
}
|
||||
$items = $data['emojis'] ?? [];
|
||||
|
||||
$map = [];
|
||||
foreach ($items as $item) {
|
||||
$category = (string) ($item['category'] ?? '');
|
||||
$subcategory = (string) ($item['subcategory'] ?? '');
|
||||
if ($category === '' || $subcategory === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$category] ??= [];
|
||||
$map[$category][$subcategory] = true;
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($map as $category => $subcategories) {
|
||||
$out[$category] = array_keys($subcategories);
|
||||
sort($out[$category], SORT_NATURAL | SORT_FLAG_CASE);
|
||||
}
|
||||
ksort($out, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
return $this->jsonWithTier($out, $tier);
|
||||
}
|
||||
|
||||
public function emojis(Request $request): JsonResponse
|
||||
{
|
||||
$tier = $this->detectTier($request);
|
||||
try {
|
||||
$data = $this->loadData();
|
||||
} catch (RuntimeException $e) {
|
||||
return $this->jsonWithTier([
|
||||
'error' => 'data_load_failed',
|
||||
'message' => $e->getMessage(),
|
||||
], $tier, 500);
|
||||
}
|
||||
$items = $data['emojis'] ?? [];
|
||||
|
||||
$q = trim((string) ($request->query('q', $request->query('query', ''))));
|
||||
$category = trim((string) $request->query('category', ''));
|
||||
$subcategory = trim((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.max_limit', 50), 1);
|
||||
$limit = min(max((int) $request->query('limit', $defaultLimit), 1), $maxLimit);
|
||||
|
||||
$filtered = array_values(array_filter($items, function (array $item) use ($q, $category, $subcategory): bool {
|
||||
if ($category !== '' && strcasecmp((string) ($item['category'] ?? ''), $category) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subcategory !== '' && strcasecmp((string) ($item['subcategory'] ?? ''), $subcategory) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($q === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$haystack = strtolower(implode(' ', [
|
||||
(string) ($item['emoji'] ?? ''),
|
||||
(string) ($item['name'] ?? ''),
|
||||
(string) ($item['slug'] ?? ''),
|
||||
(string) ($item['category'] ?? ''),
|
||||
(string) ($item['subcategory'] ?? ''),
|
||||
implode(' ', $item['keywords_en'] ?? []),
|
||||
implode(' ', $item['keywords_id'] ?? []),
|
||||
implode(' ', $item['aliases'] ?? []),
|
||||
implode(' ', $item['shortcodes'] ?? []),
|
||||
implode(' ', $item['alt_shortcodes'] ?? []),
|
||||
implode(' ', $item['intent_tags'] ?? []),
|
||||
]));
|
||||
|
||||
return str_contains($haystack, strtolower($q));
|
||||
}));
|
||||
|
||||
$total = count($filtered);
|
||||
$offset = ($page - 1) * $limit;
|
||||
$pageItems = array_slice($filtered, $offset, $limit);
|
||||
|
||||
$outItems = array_map(fn (array $item): array => $this->transformItem($item, $tier), $pageItems);
|
||||
|
||||
return $this->jsonWithTier([
|
||||
'items' => $outItems,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
], $tier);
|
||||
}
|
||||
|
||||
private function detectTier(Request $request): string
|
||||
{
|
||||
$key = trim((string) $request->bearerToken());
|
||||
if ($key === '') {
|
||||
$key = trim((string) $request->header('X-License-Key', ''));
|
||||
}
|
||||
if ($key === '') {
|
||||
return self::TIER_FREE;
|
||||
}
|
||||
|
||||
if ((bool) config('dewemoji.license.accept_all', false)) {
|
||||
return self::TIER_PRO;
|
||||
}
|
||||
|
||||
$validKeys = config('dewemoji.license.pro_keys', []);
|
||||
if (is_array($validKeys) && in_array($key, $validKeys, true)) {
|
||||
return self::TIER_PRO;
|
||||
}
|
||||
|
||||
return self::TIER_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $item
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function transformItem(array $item, string $tier): array
|
||||
{
|
||||
$out = [
|
||||
'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),
|
||||
];
|
||||
|
||||
if ($tier === self::TIER_PRO) {
|
||||
$out += [
|
||||
'unified' => (string) ($item['unified'] ?? ''),
|
||||
'codepoints' => $item['codepoints'] ?? [],
|
||||
'shortcodes' => $item['shortcodes'] ?? [],
|
||||
'aliases' => $item['aliases'] ?? [],
|
||||
'keywords_en' => $item['keywords_en'] ?? [],
|
||||
'keywords_id' => $item['keywords_id'] ?? [],
|
||||
'related' => $item['related'] ?? [],
|
||||
'intent_tags' => $item['intent_tags'] ?? [],
|
||||
'description' => (string) ($item['description'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
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), " ,.;:-").'…';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
private function jsonWithTier(array $payload, string $tier, int $status = 200): JsonResponse
|
||||
{
|
||||
return response()
|
||||
->json($payload, $status, [
|
||||
'X-Dewemoji-Tier' => $tier,
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-License-Key, X-Account-Id, X-Dewemoji-Frontend',
|
||||
]);
|
||||
}
|
||||
}
|
||||
76
app/app/Http/Controllers/Api/V1/LicenseController.php
Normal file
76
app/app/Http/Controllers/Api/V1/LicenseController.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LicenseController extends Controller
|
||||
{
|
||||
public function verify(Request $request): JsonResponse
|
||||
{
|
||||
$key = trim((string) $request->input('key', ''));
|
||||
$accountId = trim((string) $request->input('account_id', ''));
|
||||
$version = trim((string) $request->input('version', ''));
|
||||
|
||||
if ($key === '') {
|
||||
return $this->response([
|
||||
'ok' => false,
|
||||
'error' => 'missing_key',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($accountId === '') {
|
||||
return $this->response([
|
||||
'ok' => false,
|
||||
'error' => 'missing_account_id',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($version === '') {
|
||||
return $this->response([
|
||||
'ok' => false,
|
||||
'error' => 'missing_version',
|
||||
]);
|
||||
}
|
||||
|
||||
$valid = $this->isValidKey($key);
|
||||
|
||||
return $this->response([
|
||||
'ok' => $valid,
|
||||
'tier' => $valid ? 'pro' : 'free',
|
||||
'error' => $valid ? null : 'invalid_license',
|
||||
]);
|
||||
}
|
||||
|
||||
private function isValidKey(string $key): bool
|
||||
{
|
||||
if ((bool) config('dewemoji.license.accept_all', false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$validKeys = config('dewemoji.license.pro_keys', []);
|
||||
if (!is_array($validKeys)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($key, $validKeys, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
private function response(array $payload, int $status = 200): JsonResponse
|
||||
{
|
||||
$tier = ($payload['ok'] ?? false) ? 'pro' : 'free';
|
||||
|
||||
return response()->json($payload, $status, [
|
||||
'X-Dewemoji-Tier' => $tier,
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-License-Key, X-Account-Id, X-Dewemoji-Frontend',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user