Files

229 lines
7.7 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* สาธารณะ — ระบบ "รหัสผู้เล่น" สำหรับ sync โปรไฟล์ข้าม browser/เครื่อง (Guest)
* ผูกกับ jdPlayerKey (guest account: providerUserId = key) เหมือน player-coins.php
*
* GET ?action=code&playerKey=...&displayName=...&agentId=...
* → คืนรหัสของบัญชีนี้ (สร้างถ้ายังไม่มี) + อัปเดตชื่อ/agent ฝั่ง server
* { ok, code, codeDisplay }
*
* POST { action:'redeem', code }
* → ดึงโปรไฟล์จากรหัส → คืน playerKey + ข้อมูลโปรไฟล์ของบัญชีนั้น
* { ok, playerKey, coins, colorThemeIndex, skinToneIndex, displayName, agentId }
*
* รหัสที่ไม่ถูกใช้เกิน 30 วัน → หมดอายุ (ลบแบบ lazy ตอนมีการเรียก)
*/
require __DIR__ . '/_common.php';
const LINK_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; // ตัด 0 O 1 I L ที่กำกวม
const LINK_CODE_LEN = 8;
const LINK_CODE_TTL_DAYS = 30;
function valid_player_key(string $key): bool
{
return (bool)preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key);
}
function sanitize_display_name(string $raw): string
{
$s = trim($raw);
$s = preg_replace('/[\x00-\x1f\x7f]/u', '', $s) ?? '';
if (function_exists('mb_substr')) {
$s = mb_substr($s, 0, 24);
} else {
$s = substr($s, 0, 24);
}
return $s;
}
function sanitize_agent_id(string $raw): string
{
return preg_match('/^\d{6}$/', trim($raw)) ? trim($raw) : '';
}
function normalize_code(string $raw): string
{
$s = strtoupper(trim($raw));
$s = str_replace([' ', '-', '_'], '', $s);
// กันสับสน: ผู้ใช้พิมพ์ 0/O/1/I/L → map เข้าตัวที่ใช้จริง
$s = strtr($s, ['0' => 'O', '1' => 'I']);
return $s;
}
function generate_code(): string
{
$n = strlen(LINK_CODE_ALPHABET);
$out = '';
for ($i = 0; $i < LINK_CODE_LEN; $i++) {
$out .= LINK_CODE_ALPHABET[random_int(0, $n - 1)];
}
return $out;
}
function format_code_display(string $code): string
{
if (strlen($code) === 8) {
return substr($code, 0, 4) . '-' . substr($code, 4, 4);
}
return $code;
}
function code_is_expired($lastUsedAt): bool
{
if (!$lastUsedAt) {
return false;
}
$ts = strtotime((string)$lastUsedAt);
if ($ts === false) {
return false;
}
return (time() - $ts) > (LINK_CODE_TTL_DAYS * 86400);
}
/** ลบรหัสที่หมดอายุออกจากทุกบัญชี (lazy GC) — แก้ใน $store ตรง ๆ */
function gc_expired_codes(array &$store): bool
{
$changed = false;
foreach ($store['accounts'] as $i => $a) {
if (!empty($a['linkCode']) && code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
unset($store['accounts'][$i]['linkCode']);
$store['accounts'][$i]['linkCodeLastUsedAt'] = null;
$changed = true;
}
}
return $changed;
}
function code_in_use(array $store, string $code, ?string $exceptKey = null): bool
{
foreach ($store['accounts'] as $a) {
if (($a['linkCode'] ?? '') !== $code) {
continue;
}
if ($exceptKey !== null && ($a['providerUserId'] ?? '') === $exceptKey) {
continue;
}
if (!code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
return true;
}
}
return false;
}
function profile_payload(array $a): array
{
return [
'playerKey' => $a['providerUserId'] ?? '',
'coins' => max(0, (int)($a['coins'] ?? 0)),
'colorThemeIndex' => ($v = (int)($a['lobbyColorThemeIndex'] ?? 0)) >= 1 && $v <= 8 ? $v : null,
'skinToneIndex' => ($s = (int)($a['lobbySkinToneIndex'] ?? 0)) >= 1 && $s <= 3 ? $s : null,
'displayName' => (string)($a['displayName'] ?? ''),
'agentId' => (string)($a['agentDisplayId'] ?? ''),
];
}
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if ($method === 'GET') {
$action = (string)($_GET['action'] ?? 'code');
if ($action !== 'code') {
json_response(['ok' => false, 'error' => 'unknown action'], 400);
}
$key = trim((string)($_GET['playerKey'] ?? ''));
if (!valid_player_key($key)) {
json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8128 (a-z 0-9 _ -)'], 400);
}
$displayName = sanitize_display_name((string)($_GET['displayName'] ?? ''));
$agentId = sanitize_agent_id((string)($_GET['agentId'] ?? ''));
$store = read_store();
gc_expired_codes($store);
$now = gmdate('c');
$idx = -1;
foreach ($store['accounts'] as $i => $a) {
if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) {
$idx = $i;
break;
}
}
if ($idx < 0) {
$store['accounts'][] = [
'id' => new_id(),
'email' => '',
'displayName' => $displayName,
'loginType' => 'guest',
'providerUserId' => $key,
'notes' => 'auto: player-link',
'blocked' => false,
'coins' => 0,
'agentDisplayId' => $agentId,
'createdAt' => $now,
'updatedAt' => $now,
];
$idx = count($store['accounts']) - 1;
} else {
if ($displayName !== '') {
$store['accounts'][$idx]['displayName'] = $displayName;
}
if ($agentId !== '') {
$store['accounts'][$idx]['agentDisplayId'] = $agentId;
}
$store['accounts'][$idx]['updatedAt'] = $now;
}
$code = (string)($store['accounts'][$idx]['linkCode'] ?? '');
if ($code === '' || code_is_expired($store['accounts'][$idx]['linkCodeLastUsedAt'] ?? null)) {
do {
$code = generate_code();
} while (code_in_use($store, $code, $key));
$store['accounts'][$idx]['linkCode'] = $code;
}
$store['accounts'][$idx]['linkCodeLastUsedAt'] = $now;
if (!write_store($store)) {
json_response(['ok' => false, 'error' => 'สร้างรหัสไม่สำเร็จ'], 500);
}
json_response(['ok' => true, 'code' => $code, 'codeDisplay' => format_code_display($code)]);
}
if ($method === 'POST') {
$body = require_json_body();
$action = (string)($body['action'] ?? 'redeem');
if ($action !== 'redeem') {
json_response(['ok' => false, 'error' => 'unknown action'], 400);
}
$code = normalize_code((string)($body['code'] ?? ''));
if (!preg_match('/^[A-Z2-9]{' . LINK_CODE_LEN . '}$/', $code)) {
json_response(['ok' => false, 'error' => 'รูปแบบรหัสไม่ถูกต้อง'], 400);
}
$store = read_store();
$sweep = gc_expired_codes($store);
$now = gmdate('c');
$found = -1;
foreach ($store['accounts'] as $i => $a) {
if (($a['linkCode'] ?? '') === $code && !code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
$found = $i;
break;
}
}
if ($found < 0) {
if ($sweep) {
write_store($store);
}
json_response(['ok' => false, 'error' => 'ไม่พบรหัสนี้ หรือรหัสหมดอายุแล้ว'], 404);
}
$store['accounts'][$found]['linkCodeLastUsedAt'] = $now;
$store['accounts'][$found]['updatedAt'] = $now;
write_store($store);
$payload = profile_payload($store['accounts'][$found]);
$payload['ok'] = true;
json_response($payload);
}
json_response(['ok' => false, 'error' => 'Use GET or POST'], 405);