chore: change Android Apk download status to upcoming

This commit is contained in:
Dwindi Ramadhana
2026-06-14 15:47:28 +07:00
parent 88218c7798
commit c3ce549264

View File

@@ -18,26 +18,31 @@ class SiteController extends Controller
{ {
/** @var array<string,string> */ /** @var array<string,string> */
private const CATEGORY_TO_SLUG = [ private const CATEGORY_TO_SLUG = [
'Smileys & Emotion' => 'smileys', "Smileys & Emotion" => "smileys",
'People & Body' => 'people', "People & Body" => "people",
'Animals & Nature' => 'animals', "Animals & Nature" => "animals",
'Food & Drink' => 'food', "Food & Drink" => "food",
'Travel & Places' => 'travel', "Travel & Places" => "travel",
'Activities' => 'activities', "Activities" => "activities",
'Objects' => 'objects', "Objects" => "objects",
'Symbols' => 'symbols', "Symbols" => "symbols",
'Flags' => 'flags', "Flags" => "flags",
]; ];
private function billingMode(): string private function billingMode(): string
{ {
$settings = app(SettingsService::class); $settings = app(SettingsService::class);
$preferred = (string) ($settings->get('billing_mode', config('dewemoji.billing.mode', 'sandbox')) ?: 'sandbox'); $preferred =
(string) ($settings->get(
"billing_mode",
config("dewemoji.billing.mode", "sandbox"),
) ?:
"sandbox");
if ($this->paypalConfiguredMode($preferred)) { if ($this->paypalConfiguredMode($preferred)) {
return $preferred; return $preferred;
} }
$fallback = $preferred === 'live' ? 'sandbox' : 'live'; $fallback = $preferred === "live" ? "sandbox" : "live";
if ($this->paypalConfiguredMode($fallback)) { if ($this->paypalConfiguredMode($fallback)) {
return $fallback; return $fallback;
} }
@@ -47,111 +52,122 @@ class SiteController extends Controller
public function home(Request $request): View public function home(Request $request): View
{ {
return view('site.home', [ return view("site.home", [
'initialQuery' => trim((string) $request->query('q', '')), "initialQuery" => trim((string) $request->query("q", "")),
'initialCategory' => trim((string) $request->query('category', '')), "initialCategory" => trim((string) $request->query("category", "")),
'initialSubcategory' => trim((string) $request->query('subcategory', '')), "initialSubcategory" => trim(
'canonicalPath' => '/', (string) $request->query("subcategory", ""),
'userTier' => $request->user()?->tier, ),
"canonicalPath" => "/",
"userTier" => $request->user()?->tier,
]); ]);
} }
public function browse(Request $request): RedirectResponse|View public function browse(Request $request): RedirectResponse|View
{ {
$cat = strtolower(trim((string) $request->query('cat', 'all'))); $cat = strtolower(trim((string) $request->query("cat", "all")));
if ($cat !== '' && $cat !== 'all' && array_key_exists($cat, $this->categorySlugMap())) { if (
return redirect('/'.$cat, 301); $cat !== "" &&
$cat !== "all" &&
array_key_exists($cat, $this->categorySlugMap())
) {
return redirect("/" . $cat, 301);
} }
return view('site.home', [ return view("site.home", [
'initialQuery' => trim((string) $request->query('q', '')), "initialQuery" => trim((string) $request->query("q", "")),
'initialCategory' => trim((string) $request->query('category', '')), "initialCategory" => trim((string) $request->query("category", "")),
'initialSubcategory' => trim((string) $request->query('subcategory', '')), "initialSubcategory" => trim(
'canonicalPath' => '/browse', (string) $request->query("subcategory", ""),
'userTier' => $request->user()?->tier, ),
"canonicalPath" => "/browse",
"userTier" => $request->user()?->tier,
]); ]);
} }
public function category(string $categorySlug): View public function category(string $categorySlug): View
{ {
if ($categorySlug === 'all') { if ($categorySlug === "all") {
return view('site.home', [ return view("site.home", [
'initialQuery' => '', "initialQuery" => "",
'initialCategory' => '', "initialCategory" => "",
'initialSubcategory' => '', "initialSubcategory" => "",
'canonicalPath' => '/', "canonicalPath" => "/",
'userTier' => request()->user()?->tier, "userTier" => request()->user()?->tier,
]); ]);
} }
$categoryLabel = $this->categorySlugMap()[$categorySlug] ?? ''; $categoryLabel = $this->categorySlugMap()[$categorySlug] ?? "";
abort_if($categoryLabel === '', 404); abort_if($categoryLabel === "", 404);
return view('site.home', [ return view("site.home", [
'initialQuery' => '', "initialQuery" => "",
'initialCategory' => $categoryLabel, "initialCategory" => $categoryLabel,
'initialSubcategory' => '', "initialSubcategory" => "",
'canonicalPath' => '/'.$categorySlug, "canonicalPath" => "/" . $categorySlug,
'userTier' => request()->user()?->tier, "userTier" => request()->user()?->tier,
]); ]);
} }
public function categorySubcategory(string $categorySlug, string $subcategorySlug): View public function categorySubcategory(
{ string $categorySlug,
if ($categorySlug === 'all') { string $subcategorySlug,
): View {
if ($categorySlug === "all") {
abort(404); abort(404);
} }
$categoryLabel = $this->categorySlugMap()[$categorySlug] ?? ''; $categoryLabel = $this->categorySlugMap()[$categorySlug] ?? "";
abort_if($categoryLabel === '', 404); abort_if($categoryLabel === "", 404);
return view('site.home', [ return view("site.home", [
'initialQuery' => '', "initialQuery" => "",
'initialCategory' => $categoryLabel, "initialCategory" => $categoryLabel,
'initialSubcategory' => $subcategorySlug, "initialSubcategory" => $subcategorySlug,
'canonicalPath' => '/'.$categorySlug.'/'.$subcategorySlug, "canonicalPath" => "/" . $categorySlug . "/" . $subcategorySlug,
'userTier' => request()->user()?->tier, "userTier" => request()->user()?->tier,
]); ]);
} }
public function apiDocs(): View public function apiDocs(): View
{ {
return view('site.api-docs'); return view("site.api-docs");
} }
public function pricing(): View public function pricing(): View
{ {
$user = request()->user(); $user = request()->user();
$currencyPref = strtoupper((string) session('pricing_currency', '')); $currencyPref = strtoupper((string) session("pricing_currency", ""));
if (!in_array($currencyPref, ['IDR', 'USD'], true)) { if (!in_array($currencyPref, ["IDR", "USD"], true)) {
$currencyPref = $this->detectPricingCurrency(request()); $currencyPref = $this->detectPricingCurrency(request());
session(['pricing_currency' => $currencyPref]); session(["pricing_currency" => $currencyPref]);
} }
$rate = (int) config('dewemoji.pricing.usd_rate', 15000); $rate = (int) config("dewemoji.pricing.usd_rate", 15000);
$plans = PricingPlan::where('status', 'active')->get()->keyBy('code'); $plans = PricingPlan::where("status", "active")->get()->keyBy("code");
$defaults = config('dewemoji.pricing.defaults', []); $defaults = config("dewemoji.pricing.defaults", []);
$fallback = collect($defaults)->keyBy('code'); $fallback = collect($defaults)->keyBy("code");
$getPlanAmount = function (string $code) use ($plans, $fallback): int { $getPlanAmount = function (string $code) use ($plans, $fallback): int {
$plan = $plans->get($code) ?? $fallback->get($code); $plan = $plans->get($code) ?? $fallback->get($code);
return (int) ($plan['amount'] ?? $plan->amount ?? 0); return (int) ($plan["amount"] ?? ($plan->amount ?? 0));
}; };
$pricing = [ $pricing = [
'personal_monthly' => [ "personal_monthly" => [
'idr' => $getPlanAmount('personal_monthly'), "idr" => $getPlanAmount("personal_monthly"),
], ],
'personal_annual' => [ "personal_annual" => [
'idr' => $getPlanAmount('personal_annual'), "idr" => $getPlanAmount("personal_annual"),
], ],
'personal_lifetime' => [ "personal_lifetime" => [
'idr' => $getPlanAmount('personal_lifetime'), "idr" => $getPlanAmount("personal_lifetime"),
], ],
]; ];
foreach ($pricing as $key => $row) { foreach ($pricing as $key => $row) {
$pricing[$key]['usd'] = $rate > 0 ? round($row['idr'] / $rate, 2) : 0; $pricing[$key]["usd"] =
$rate > 0 ? round($row["idr"] / $rate, 2) : 0;
} }
$hasActiveLifetime = false; $hasActiveLifetime = false;
@@ -159,49 +175,75 @@ class SiteController extends Controller
$pendingCooldownRemaining = 0; $pendingCooldownRemaining = 0;
if ($user) { if ($user) {
$hasActiveLifetime = Subscription::query() $hasActiveLifetime = Subscription::query()
->where('user_id', $user->id) ->where("user_id", $user->id)
->where('plan', 'personal_lifetime') ->where("plan", "personal_lifetime")
->where('status', 'active') ->where("status", "active")
->where(function ($query) { ->where(function ($query) {
$query->whereNull('expires_at') $query
->orWhere('expires_at', '>', now()); ->whereNull("expires_at")
->orWhere("expires_at", ">", now());
}) })
->exists(); ->exists();
$hasPendingPayment = Payment::query() $hasPendingPayment = Payment::query()
->where('user_id', $user->id) ->where("user_id", $user->id)
->where('status', 'pending') ->where("status", "pending")
->exists(); ->exists();
$cooldown = (int) config('dewemoji.billing.pending_cooldown_seconds', 120); $cooldown = (int) config(
"dewemoji.billing.pending_cooldown_seconds",
120,
);
if ($cooldown > 0) { if ($cooldown > 0) {
$latestPending = Payment::query() $latestPending = Payment::query()
->where('user_id', $user->id) ->where("user_id", $user->id)
->where('status', 'pending') ->where("status", "pending")
->orderByDesc('id') ->orderByDesc("id")
->first(); ->first();
if ($latestPending && $latestPending->created_at) { if ($latestPending && $latestPending->created_at) {
$age = max(0, now()->getTimestamp() - $latestPending->created_at->getTimestamp()); $age = max(
0,
now()->getTimestamp() -
$latestPending->created_at->getTimestamp(),
);
$pendingCooldownRemaining = max(0, $cooldown - $age); $pendingCooldownRemaining = max(0, $cooldown - $age);
} }
} }
} }
return view('site.pricing', [ return view("site.pricing", [
'currencyPref' => $currencyPref, "currencyPref" => $currencyPref,
'usdRate' => $rate, "usdRate" => $rate,
'pricing' => $pricing, "pricing" => $pricing,
'payments' => [ "payments" => [
'qris_url' => (string) config('dewemoji.payments.qris_url', ''), "qris_url" => (string) config("dewemoji.payments.qris_url", ""),
'paypal_url' => (string) config('dewemoji.payments.paypal_url', ''), "paypal_url" => (string) config(
"dewemoji.payments.paypal_url",
"",
),
], ],
'pakasirEnabled' => (bool) config('dewemoji.billing.providers.pakasir.enabled', false) "pakasirEnabled" =>
&& (string) config('dewemoji.billing.providers.pakasir.api_base', '') !== '' (bool) config(
&& (string) config('dewemoji.billing.providers.pakasir.api_key', '') !== '' "dewemoji.billing.providers.pakasir.enabled",
&& (string) config('dewemoji.billing.providers.pakasir.project', '') !== '', false,
'paypalEnabled' => $this->paypalEnabled($this->billingMode()), ) &&
'paypalPlans' => $this->paypalPlanAvailability($this->billingMode()), (string) config(
'hasActiveLifetime' => $hasActiveLifetime, "dewemoji.billing.providers.pakasir.api_base",
'hasPendingPayment' => $hasPendingPayment, "",
'pendingCooldownRemaining' => $pendingCooldownRemaining, ) !== "" &&
(string) config(
"dewemoji.billing.providers.pakasir.api_key",
"",
) !== "" &&
(string) config(
"dewemoji.billing.providers.pakasir.project",
"",
) !== "",
"paypalEnabled" => $this->paypalEnabled($this->billingMode()),
"paypalPlans" => $this->paypalPlanAvailability(
$this->billingMode(),
),
"hasActiveLifetime" => $hasActiveLifetime,
"hasPendingPayment" => $hasPendingPayment,
"pendingCooldownRemaining" => $pendingCooldownRemaining,
]); ]);
} }
@@ -211,23 +253,43 @@ class SiteController extends Controller
return true; return true;
} }
$fallback = $mode === 'live' ? 'sandbox' : 'live'; $fallback = $mode === "live" ? "sandbox" : "live";
return $this->paypalConfiguredMode($fallback); return $this->paypalConfiguredMode($fallback);
} }
private function paypalConfiguredMode(string $mode): bool private function paypalConfiguredMode(string $mode): bool
{ {
$enabled = (bool) config('dewemoji.billing.providers.paypal.enabled', false); $enabled = (bool) config(
$clientId = (string) config("dewemoji.billing.providers.paypal.{$mode}.client_id", ''); "dewemoji.billing.providers.paypal.enabled",
$clientSecret = (string) config("dewemoji.billing.providers.paypal.{$mode}.client_secret", ''); false,
$apiBase = (string) config("dewemoji.billing.providers.paypal.{$mode}.api_base", ''); );
$clientId = (string) config(
"dewemoji.billing.providers.paypal.{$mode}.client_id",
"",
);
$clientSecret = (string) config(
"dewemoji.billing.providers.paypal.{$mode}.client_secret",
"",
);
$apiBase = (string) config(
"dewemoji.billing.providers.paypal.{$mode}.api_base",
"",
);
return $enabled && $clientId !== '' && $clientSecret !== '' && $apiBase !== ''; return $enabled &&
$clientId !== "" &&
$clientSecret !== "" &&
$apiBase !== "";
} }
private function paypalPlanAvailability(string $mode): array private function paypalPlanAvailability(string $mode): array
{ {
$plans = PricingPlan::whereIn('code', ['personal_monthly', 'personal_annual'])->get()->keyBy('code'); $plans = PricingPlan::whereIn("code", [
"personal_monthly",
"personal_annual",
])
->get()
->keyBy("code");
$fromDb = function (string $code) use ($plans, $mode): bool { $fromDb = function (string $code) use ($plans, $mode): bool {
$plan = $plans->get($code); $plan = $plans->get($code);
@@ -235,157 +297,188 @@ class SiteController extends Controller
return false; return false;
} }
$meta = $plan->meta ?? []; $meta = $plan->meta ?? [];
return (string) ($meta['paypal'][$mode]['plan']['id'] ?? '') !== ''; return (string) ($meta["paypal"][$mode]["plan"]["id"] ?? "") !== "";
}; };
$fromEnv = function (string $code) use ($mode): bool { $fromEnv = function (string $code) use ($mode): bool {
return (string) config("dewemoji.billing.providers.paypal.plan_ids.{$mode}.{$code}", '') !== ''; return (string) config(
"dewemoji.billing.providers.paypal.plan_ids.{$mode}.{$code}",
"",
) !== "";
}; };
return [ return [
'personal_monthly' => $fromDb('personal_monthly') || $fromEnv('personal_monthly'), "personal_monthly" =>
'personal_annual' => $fromDb('personal_annual') || $fromEnv('personal_annual'), $fromDb("personal_monthly") || $fromEnv("personal_monthly"),
"personal_annual" =>
$fromDb("personal_annual") || $fromEnv("personal_annual"),
]; ];
} }
public function setPricingCurrency(Request $request): RedirectResponse public function setPricingCurrency(Request $request): RedirectResponse
{ {
$data = $request->validate([ $data = $request->validate([
'currency' => 'required|string|in:IDR,USD', "currency" => "required|string|in:IDR,USD",
]); ]);
session(['pricing_currency' => $data['currency']]); session(["pricing_currency" => $data["currency"]]);
return back(); return back();
} }
public function support(): View public function support(): View
{ {
return view('site.support'); return view("site.support");
} }
public function download(): View public function download(): View
{ {
$downloadBaseUrl = rtrim((string) config('dewemoji.apk_release.public_base_url', ''), '/'); $downloadBaseUrl = rtrim(
$androidEnabled = (bool) config('dewemoji.apk_release.enabled', false) && $downloadBaseUrl !== ''; (string) config("dewemoji.apk_release.public_base_url", ""),
"/",
);
$androidEnabled = false;
return view('site.download', [ return view("site.download", [
'androidEnabled' => $androidEnabled, "androidEnabled" => $androidEnabled,
'androidVersionJsonUrl' => $androidEnabled ? $downloadBaseUrl.'/version.json' : '', "androidVersionJsonUrl" => $androidEnabled
'androidLatestApkUrl' => $androidEnabled ? $downloadBaseUrl.'/dewemoji-latest.apk' : '', ? $downloadBaseUrl . "/version.json"
: "",
"androidLatestApkUrl" => $androidEnabled
? $downloadBaseUrl . "/dewemoji-latest.apk"
: "",
]); ]);
} }
public function downloadVersionJson(Request $request): RedirectResponse|JsonResponse public function downloadVersionJson(
{ Request $request,
$target = $this->apkReleaseTargetUrl('version_json'); ): RedirectResponse|JsonResponse {
if ($target === '') { $target = $this->apkReleaseTargetUrl("version_json");
return response()->json(['ok' => false, 'error' => 'apk_release_not_configured'], 404); if ($target === "") {
return response()->json(
["ok" => false, "error" => "apk_release_not_configured"],
404,
);
} }
return redirect()->away($target, 302, [ return redirect()->away($target, 302, [
'Cache-Control' => 'no-store, no-cache, must-revalidate', "Cache-Control" => "no-store, no-cache, must-revalidate",
'Pragma' => 'no-cache', "Pragma" => "no-cache",
]); ]);
} }
public function downloadLatestApk(Request $request): RedirectResponse|JsonResponse public function downloadLatestApk(
{ Request $request,
$target = $this->apkReleaseTargetUrl('latest_apk'); ): RedirectResponse|JsonResponse {
if ($target === '') { $target = $this->apkReleaseTargetUrl("latest_apk");
return response()->json(['ok' => false, 'error' => 'apk_release_not_configured'], 404); if ($target === "") {
return response()->json(
["ok" => false, "error" => "apk_release_not_configured"],
404,
);
} }
return redirect()->away($target, 302, [ return redirect()->away($target, 302, [
'Cache-Control' => 'no-store, no-cache, must-revalidate', "Cache-Control" => "no-store, no-cache, must-revalidate",
'Pragma' => 'no-cache', "Pragma" => "no-cache",
]); ]);
} }
public function assetLinks(): JsonResponse public function assetLinks(): JsonResponse
{ {
$appId = trim((string) config('dewemoji.apk_release.app_id', '')); $appId = trim((string) config("dewemoji.apk_release.app_id", ""));
$rawFingerprints = (array) config('dewemoji.apk_release.assetlinks.fingerprints', []); $rawFingerprints = (array) config(
"dewemoji.apk_release.assetlinks.fingerprints",
[],
);
$fingerprints = []; $fingerprints = [];
foreach ($rawFingerprints as $fingerprint) { foreach ($rawFingerprints as $fingerprint) {
$normalized = $this->normalizeApkCertFingerprint((string) $fingerprint); $normalized = $this->normalizeApkCertFingerprint(
if ($normalized !== '') { (string) $fingerprint,
);
if ($normalized !== "") {
$fingerprints[] = $normalized; $fingerprints[] = $normalized;
} }
} }
$fingerprints = array_values(array_unique($fingerprints)); $fingerprints = array_values(array_unique($fingerprints));
if ($appId === '' || $fingerprints === []) { if ($appId === "" || $fingerprints === []) {
return response()->json([], 200, [ return response()->json([], 200, [
'Cache-Control' => 'no-store, no-cache, must-revalidate', "Cache-Control" => "no-store, no-cache, must-revalidate",
'Pragma' => 'no-cache', "Pragma" => "no-cache",
]); ]);
} }
return response()->json([ return response()->json(
[ [
'relation' => [ [
'delegate_permission/common.handle_all_urls', "relation" => [
"delegate_permission/common.handle_all_urls",
], ],
'target' => [ "target" => [
'namespace' => 'android_app', "namespace" => "android_app",
'package_name' => $appId, "package_name" => $appId,
'sha256_cert_fingerprints' => $fingerprints, "sha256_cert_fingerprints" => $fingerprints,
], ],
], ],
], 200, [ ],
'Cache-Control' => 'no-store, no-cache, must-revalidate', 200,
'Pragma' => 'no-cache', [
]); "Cache-Control" => "no-store, no-cache, must-revalidate",
"Pragma" => "no-cache",
],
);
} }
public function privacy(): View public function privacy(): View
{ {
return view('site.privacy'); return view("site.privacy");
} }
public function terms(): View public function terms(): View
{ {
return view('site.terms'); return view("site.terms");
} }
private function detectPricingCurrency(Request $request): string private function detectPricingCurrency(Request $request): string
{ {
$country = strtoupper((string) ($request->header('CF-IPCountry') $country = strtoupper(
?? $request->header('X-Country-Code') (string) ($request->header("CF-IPCountry") ??
?? $request->header('X-Geo-Country') ($request->header("X-Country-Code") ??
?? $request->header('X-Appengine-Country') ($request->header("X-Geo-Country") ??
?? $request->header('CloudFront-Viewer-Country') ($request->header("X-Appengine-Country") ??
?? '')); ($request->header("CloudFront-Viewer-Country") ??
""))))),
);
return $country === 'ID' ? 'IDR' : 'USD'; return $country === "ID" ? "IDR" : "USD";
} }
public function emojiDetail(string $slug): View|Response public function emojiDetail(string $slug): View|Response
{ {
$dataPath = $this->datasetPath(); $dataPath = $this->datasetPath();
if (!is_file($dataPath)) { if (!is_file($dataPath)) {
abort(500, 'Emoji dataset file not found.'); abort(500, "Emoji dataset file not found.");
} }
$raw = file_get_contents($dataPath); $raw = file_get_contents($dataPath);
if ($raw === false) { if ($raw === false) {
abort(500, 'Emoji dataset file could not be read.'); abort(500, "Emoji dataset file could not be read.");
} }
$decoded = json_decode($raw, true); $decoded = json_decode($raw, true);
if (!is_array($decoded)) { if (!is_array($decoded)) {
abort(500, 'Emoji dataset JSON is invalid.'); abort(500, "Emoji dataset JSON is invalid.");
} }
$items = $decoded['emojis'] ?? []; $items = $decoded["emojis"] ?? [];
$match = null; $match = null;
$byEmoji = []; $byEmoji = [];
foreach ($items as $item) { foreach ($items as $item) {
$char = (string) ($item['emoji'] ?? ''); $char = (string) ($item["emoji"] ?? "");
if ($char !== '' && !isset($byEmoji[$char])) { if ($char !== "" && !isset($byEmoji[$char])) {
$byEmoji[$char] = $item; $byEmoji[$char] = $item;
} }
if (($item['slug'] ?? '') === $slug) { if (($item["slug"] ?? "") === $slug) {
$match = $item; $match = $item;
} }
} }
@@ -395,98 +488,143 @@ class SiteController extends Controller
} }
$relatedDetails = []; $relatedDetails = [];
foreach (array_slice($match['related'] ?? [], 0, 8) as $relatedEmoji) { foreach (array_slice($match["related"] ?? [], 0, 8) as $relatedEmoji) {
$relatedEmoji = (string) $relatedEmoji; $relatedEmoji = (string) $relatedEmoji;
$ref = $byEmoji[$relatedEmoji] ?? null; $ref = $byEmoji[$relatedEmoji] ?? null;
$relatedDetails[] = [ $relatedDetails[] = [
'emoji' => $relatedEmoji, "emoji" => $relatedEmoji,
'slug' => (string) ($ref['slug'] ?? ''), "slug" => (string) ($ref["slug"] ?? ""),
'name' => (string) ($ref['name'] ?? $relatedEmoji), "name" => (string) ($ref["name"] ?? $relatedEmoji),
]; ];
} }
$user = request()->user(); $user = request()->user();
$canManageKeywords = (bool) $user; $canManageKeywords = (bool) $user;
$isPersonal = $user && (string) $user->tier === 'personal'; $isPersonal = $user && (string) $user->tier === "personal";
$freeLimit = (int) config('dewemoji.pagination.free_max_limit', 20); $freeLimit = (int) config("dewemoji.pagination.free_max_limit", 20);
$keywordLimit = $isPersonal ? null : $freeLimit; $keywordLimit = $isPersonal ? null : $freeLimit;
$userKeywords = []; $userKeywords = [];
$activeKeywordCount = 0; $activeKeywordCount = 0;
if ($canManageKeywords) { if ($canManageKeywords) {
$activeKeywordCount = UserKeyword::where('user_id', $user->id) $activeKeywordCount = UserKeyword::where("user_id", $user->id)
->where('is_active', true) ->where("is_active", true)
->count(); ->count();
$userKeywords = UserKeyword::where('user_id', $user->id) $userKeywords = UserKeyword::where("user_id", $user->id)
->where('emoji_slug', $slug) ->where("emoji_slug", $slug)
->orderByDesc('id') ->orderByDesc("id")
->get(); ->get();
} }
$limitReached = $keywordLimit !== null && $activeKeywordCount >= $keywordLimit; $limitReached =
$keywordLimit !== null && $activeKeywordCount >= $keywordLimit;
return view('site.emoji-detail', [ return view("site.emoji-detail", [
'emoji' => $match, "emoji" => $match,
'relatedDetails' => $relatedDetails, "relatedDetails" => $relatedDetails,
'canonicalPath' => '/emoji/'.$slug, "canonicalPath" => "/emoji/" . $slug,
'userKeywords' => $userKeywords, "userKeywords" => $userKeywords,
'canManageKeywords' => $canManageKeywords, "canManageKeywords" => $canManageKeywords,
'keywordLimit' => $keywordLimit, "keywordLimit" => $keywordLimit,
'limitReached' => $limitReached, "limitReached" => $limitReached,
'activeKeywordCount' => $activeKeywordCount, "activeKeywordCount" => $activeKeywordCount,
'userTier' => $user?->tier, "userTier" => $user?->tier,
]); ]);
} }
public function robotsTxt(): Response public function robotsTxt(): Response
{ {
$base = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/'); $base = rtrim(
$body = "User-agent: *\nAllow: /\n\nSitemap: ".$base."/sitemap.xml\n"; config("app.url", request()->getSchemeAndHttpHost()),
"/",
);
$body =
"User-agent: *\nAllow: /\n\nSitemap: " . $base . "/sitemap.xml\n";
return response($body, 200)->header('Content-Type', 'text/plain; charset=UTF-8'); return response($body, 200)->header(
"Content-Type",
"text/plain; charset=UTF-8",
);
} }
public function sitemapXml(): Response public function sitemapXml(): Response
{ {
$data = $this->loadDataset(); $data = $this->loadDataset();
$items = is_array($data['emojis'] ?? null) ? $data['emojis'] : []; $items = is_array($data["emojis"] ?? null) ? $data["emojis"] : [];
$base = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/'); $base = rtrim(
config("app.url", request()->getSchemeAndHttpHost()),
"/",
);
$lastUpdatedTs = isset($data['last_updated_ts']) ? (int) $data['last_updated_ts'] : time(); $lastUpdatedTs = isset($data["last_updated_ts"])
$lastUpdated = gmdate('Y-m-d\TH:i:s\Z', $lastUpdatedTs); ? (int) $data["last_updated_ts"]
: time();
$lastUpdated = gmdate("Y-m-d\TH:i:s\Z", $lastUpdatedTs);
$urls = [ $urls = [
['loc' => $base.'/', 'priority' => '0.8', 'changefreq' => 'daily'], [
['loc' => $base.'/api-docs', 'priority' => '0.5', 'changefreq' => 'weekly'], "loc" => $base . "/",
['loc' => $base.'/pricing', 'priority' => '0.7', 'changefreq' => 'weekly'], "priority" => "0.8",
['loc' => $base.'/privacy', 'priority' => '0.3', 'changefreq' => 'monthly'], "changefreq" => "daily",
['loc' => $base.'/terms', 'priority' => '0.3', 'changefreq' => 'monthly'], ],
['loc' => $base.'/support', 'priority' => '0.4', 'changefreq' => 'weekly'], [
"loc" => $base . "/api-docs",
"priority" => "0.5",
"changefreq" => "weekly",
],
[
"loc" => $base . "/pricing",
"priority" => "0.7",
"changefreq" => "weekly",
],
[
"loc" => $base . "/privacy",
"priority" => "0.3",
"changefreq" => "monthly",
],
[
"loc" => $base . "/terms",
"priority" => "0.3",
"changefreq" => "monthly",
],
[
"loc" => $base . "/support",
"priority" => "0.4",
"changefreq" => "weekly",
],
]; ];
foreach ($items as $item) { foreach ($items as $item) {
$slug = trim((string) ($item['slug'] ?? '')); $slug = trim((string) ($item["slug"] ?? ""));
if ($slug === '' || $this->shouldHideForSitemap($item)) { if ($slug === "" || $this->shouldHideForSitemap($item)) {
continue; continue;
} }
$urls[] = [ $urls[] = [
'loc' => $base.'/emoji/'.$slug, "loc" => $base . "/emoji/" . $slug,
'priority' => '0.6', "priority" => "0.6",
'changefreq' => 'weekly', "changefreq" => "weekly",
]; ];
} }
$xml = '<?xml version="1.0" encoding="UTF-8"?>'."\n"; $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'."\n"; $xml .=
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' .
"\n";
foreach ($urls as $url) { foreach ($urls as $url) {
$xml .= " <url>\n"; $xml .= " <url>\n";
$xml .= ' <loc>'.htmlspecialchars((string) $url['loc'], ENT_XML1)."</loc>\n"; $xml .=
$xml .= ' <lastmod>'.$lastUpdated."</lastmod>\n"; " <loc>" .
$xml .= ' <changefreq>'.$url['changefreq']."</changefreq>\n"; htmlspecialchars((string) $url["loc"], ENT_XML1) .
$xml .= ' <priority>'.$url['priority']."</priority>\n"; "</loc>\n";
$xml .= " <lastmod>" . $lastUpdated . "</lastmod>\n";
$xml .= " <changefreq>" . $url["changefreq"] . "</changefreq>\n";
$xml .= " <priority>" . $url["priority"] . "</priority>\n";
$xml .= " </url>\n"; $xml .= " </url>\n";
} }
$xml .= '</urlset>'."\n"; $xml .= "</urlset>" . "\n";
return response($xml, 200)->header('Content-Type', 'application/xml; charset=UTF-8'); return response($xml, 200)->header(
"Content-Type",
"application/xml; charset=UTF-8",
);
} }
/** /**
@@ -509,17 +647,17 @@ class SiteController extends Controller
{ {
$dataPath = $this->datasetPath(); $dataPath = $this->datasetPath();
if (!is_file($dataPath)) { if (!is_file($dataPath)) {
return ['emojis' => []]; return ["emojis" => []];
} }
$raw = file_get_contents($dataPath); $raw = file_get_contents($dataPath);
if ($raw === false) { if ($raw === false) {
return ['emojis' => []]; return ["emojis" => []];
} }
$decoded = json_decode($raw, true); $decoded = json_decode($raw, true);
if (!is_array($decoded)) { if (!is_array($decoded)) {
return ['emojis' => []]; return ["emojis" => []];
} }
return $decoded; return $decoded;
@@ -528,45 +666,49 @@ class SiteController extends Controller
private function datasetPath(): string private function datasetPath(): string
{ {
$settings = app(SettingsService::class); $settings = app(SettingsService::class);
$activePath = (string) $settings->get('emoji_dataset_active_path', ''); $activePath = (string) $settings->get("emoji_dataset_active_path", "");
if ($activePath !== '' && is_file($activePath)) { if ($activePath !== "" && is_file($activePath)) {
return $activePath; return $activePath;
} }
return (string) config('dewemoji.data_path'); return (string) config("dewemoji.data_path");
} }
private function apkReleaseTargetUrl(string $key): string private function apkReleaseTargetUrl(string $key): string
{ {
if (!(bool) config('dewemoji.apk_release.enabled', false)) { if (!(bool) config("dewemoji.apk_release.enabled", false)) {
return ''; return "";
} }
$base = trim((string) config('dewemoji.apk_release.r2_public_base_url', '')); $base = trim(
$objectKey = trim((string) config("dewemoji.apk_release.r2_keys.{$key}", '')); (string) config("dewemoji.apk_release.r2_public_base_url", ""),
if ($base === '' || $objectKey === '') { );
return ''; $objectKey = trim(
(string) config("dewemoji.apk_release.r2_keys.{$key}", ""),
);
if ($base === "" || $objectKey === "") {
return "";
} }
return rtrim($base, '/').'/'.ltrim($objectKey, '/'); return rtrim($base, "/") . "/" . ltrim($objectKey, "/");
} }
private function normalizeApkCertFingerprint(string $value): string private function normalizeApkCertFingerprint(string $value): string
{ {
$clean = strtoupper(trim($value)); $clean = strtoupper(trim($value));
if ($clean === '') { if ($clean === "") {
return ''; return "";
} }
if (preg_match('/^[0-9A-F]{64}$/', $clean) === 1) { if (preg_match('/^[0-9A-F]{64}$/', $clean) === 1) {
return implode(':', str_split($clean, 2)); return implode(":", str_split($clean, 2));
} }
if (preg_match('/^[0-9A-F]{2}(?::[0-9A-F]{2}){31}$/', $clean) === 1) { if (preg_match('/^[0-9A-F]{2}(?::[0-9A-F]{2}){31}$/', $clean) === 1) {
return $clean; return $clean;
} }
return ''; return "";
} }
/** /**
@@ -574,39 +716,39 @@ class SiteController extends Controller
*/ */
private function shouldHideForSitemap(array $emoji): bool private function shouldHideForSitemap(array $emoji): bool
{ {
$name = strtolower(trim((string) ($emoji['name'] ?? ''))); $name = strtolower(trim((string) ($emoji["name"] ?? "")));
$category = strtolower(trim((string) ($emoji['category'] ?? ''))); $category = strtolower(trim((string) ($emoji["category"] ?? "")));
$subcategory = strtolower(trim((string) ($emoji['subcategory'] ?? ''))); $subcategory = strtolower(trim((string) ($emoji["subcategory"] ?? "")));
if ($subcategory === 'family' || str_starts_with($name, 'family:')) { if ($subcategory === "family" || str_starts_with($name, "family:")) {
return true; return true;
} }
if (preg_match('~\bwoman: beard\b~i', $name)) { if (preg_match("~\bwoman: beard\b~i", $name)) {
return true; return true;
} }
if (preg_match('~\bmen with bunny ears\b~i', $name)) { if (preg_match("~\bmen with bunny ears\b~i", $name)) {
return true; return true;
} }
if (preg_match('~\bpregnant man\b~i', $name)) { if (preg_match("~\bpregnant man\b~i", $name)) {
return true; return true;
} }
if ($category === 'people & body') { if ($category === "people & body") {
if (preg_match('~\bmen holding hands\b~i', $name)) { if (preg_match("~\bmen holding hands\b~i", $name)) {
return true; return true;
} }
if (preg_match('~\bwomen holding hands\b~i', $name)) { if (preg_match("~\bwomen holding hands\b~i", $name)) {
return true; return true;
} }
if (preg_match('~kiss:.*\bman,\s*man\b~i', $name)) { if (preg_match("~kiss:.*\bman,\s*man\b~i", $name)) {
return true; return true;
} }
if (preg_match('~kiss:.*\bwoman,\s*woman\b~i', $name)) { if (preg_match("~kiss:.*\bwoman,\s*woman\b~i", $name)) {
return true; return true;
} }
if (preg_match('~couple.*\bman,\s*man\b~i', $name)) { if (preg_match("~couple.*\bman,\s*man\b~i", $name)) {
return true; return true;
} }
if (preg_match('~couple.*\bwoman,\s*woman\b~i', $name)) { if (preg_match("~couple.*\bwoman,\s*woman\b~i", $name)) {
return true; return true;
} }
} }