function data_read(string $file): ?array { if (!file_exists($file)) return null; $json = file_get_contents($file); $data = json_decode($json, true); return is_array($data) ? $data : null; } function data_write(string $file, array $data): bool { $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if (!is_string($json)) return false; // Atomic write: pehle temp file me likho, phir rename. Do processes (cron // + admin panel) ek saath likh rahe hon to bhi file kabhi adhoori/truncated // nahi hogi — warna json_decode fail hota hai aur saara data kho jaata hai. $tmp = @tempnam(dirname($file), '.tmp_dw_'); if ($tmp !== false) { if (@file_put_contents($tmp, $json, LOCK_EX) !== false) { @chmod($tmp, 0666); if (@rename($tmp, $file)) { @chmod($file, 0666); return true; } } @unlink($tmp); } // Fallback (tempnam fail / rename fail) — kam se kam exclusive lock ke saath likho. $ok = @file_put_contents($file, $json, LOCK_EX) !== false; if ($ok) @chmod($file, 0666); return $ok; } function site_base_url(): string { $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; return $scheme . '://' . $host; } /* ---------- CSRF ---------- * Admin panel ke POST forms (khaas kar Telegram wale — jahan token save hota * hai aur groups add/remove hote hain) cross-site request se protect hone * chahiye. Token session me hota hai, form me hidden field ke through jaata * hai, aur har mutating handler me verify hota hai. */ function csrf_token(): string { if (session_status() === PHP_SESSION_NONE) @session_start(); if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) { try { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } catch (Throwable $e) { $_SESSION['csrf_token'] = sha1(uniqid('', true) . mt_rand()); } } return $_SESSION['csrf_token']; } function csrf_valid(): bool { if (session_status() === PHP_SESSION_NONE) @session_start(); $sent = (string)($_POST['csrf_token'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '')); $real = (string)($_SESSION['csrf_token'] ?? ''); return $real !== '' && $sent !== '' && hash_equals($real, $sent); } /** Render hidden CSRF input. Use inside every POST form. */ function csrf_field(): string { return ''; } function load_games(): array { return data_read(GAMES_FILE) ?? []; } function save_games(array $games): bool { return data_write(GAMES_FILE, $games); } function load_results(): array { return data_read(RESULTS_FILE) ?? []; } function save_results(array $results): bool { return data_write(RESULTS_FILE, $results); } function load_users(): array { return data_read(USERS_FILE) ?? []; } function save_users(array $users): bool { return data_write(USERS_FILE, $users); } function contact_defaults(): array { return [ 'show_your_game' => [ 'heading' => '🎀 SHOW YOUR GAME HERE 🎀', 'content' => '', 'note' => 'Contact us to show your game here', 'chat_bubble' => 'Delhi Bazar, Shri Ganesh, Faridabad, Ghaziabad, Gali and Desawar', ], 'messages' => [], ]; } function load_contact(): array { $data = data_read(CONTACT_FILE); if (!is_array($data)) return contact_defaults(); if (!isset($data['show_your_game']) || !is_array($data['show_your_game'])) { $data['show_your_game'] = contact_defaults()['show_your_game']; } if (!isset($data['messages']) || !is_array($data['messages'])) { $data['messages'] = []; } return $data; } function save_contact(array $data): bool { return data_write(CONTACT_FILE, $data); } /** * SEO content for the public pages: homepage, record chart, full chart, terms. * Templates use placeholders filled at render time via seo_apply(): * {date_js} e.g. 14th September 2026 (homepage) * {year} e.g. 2026 (homepage, chart) * {month_year} e.g. September 2026 (homepage) * {game_name} e.g. Gali (record) * {result_for} e.g. September 2026 (record, chart) * {month_name} e.g. September (chart) */ function seo_defaults(): array { return [ 'homepage' => [ 'title' => 'Satta King Result of {date_js} And Satta King Chart of {year} for Gali, Desawar, Ghaziabad and Faridabad', 'description' => 'Daily Superfast Satta King Result of {date_js} And Leak Numbers for Gali, Desawar, Ghaziabad and Faridabad With Complete Old Satta King Chart of 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2023, 2024, 2025 From Satta King Fast, Satta King Ghaziabad, Satta King Desawar, Satta King Gali, Satta King Faridabad.', 'keywords' => '', 'robots' => 'index/follow', 'ads_h1' => 'Daily Superfast Satta King Result of {date_js} And Leak Numbers for Gali, Desawar, Ghaziabad and Faridabad With Complete Old Satta King Chart of 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2023, 2024, 2025 From Satta King Fast, Satta King Ghaziabad, Satta King Desawar, Satta King Gali, Satta King Faridabad.', ], 'record' => [ 'title' => '{game_name} Satta Result Record Chart of {result_for} with Gali, Desawar, Ghaziabad and Faridabad', 'description' => '{game_name} Satta Result And Monthly Satta Chart of {result_for} With Combined Chart of Gali, Desawar, Ghaziabad, Faridabad And Shri Ganesh from Satta King Fast, Satta King Result, Satta King Chart, Black Satta King and Satta King 786.', 'keywords' => '', 'robots' => 'index/follow', 'ads_h1' => '{game_name} Satta Result And Monthly Satta Chart of {result_for} With Combined Chart of Gali, Desawar, Ghaziabad, Faridabad And Shri Ganesh from Satta King Fast.', ], 'chart' => [ 'title' => 'Satta King 786 Chart and Result of {month_name}-{year} for Gali, Desawar, Ghaziabad and Faridabad', 'description' => 'Satta King Result Chart of {month_name}-{year} And Leak Numbers for Gali, Desawar, Ghaziabad and Faridabad from Satta King Fast, Satta King 786 chart, Satta King 2024 chart, satta king desawar 2019, satta king desawar 2024, Balck Satta King 786.', 'keywords' => '', 'robots' => 'index/follow', 'ads_h1' => '{result_for} Satta King Result Chart for Gali, Desawer, Gaziabad and Faridabad from Satta King Fast.', ], 'terms' => [ 'title' => 'Terms of Service', 'description' => 'Terms of Service for Satta King Real — an independent media portal and informational archive for public draw results.', 'keywords' => 'satta king terms of service, satta king real terms', 'robots' => 'index/follow', 'ads_h1' => 'Terms of Service', 'content' => '

Effective Date: November 24, 2024

1. AGREEMENT TO TERMS

These Terms of Service (“Terms”) constitute a legally binding agreement made between you ("the User") and the owners and operators of satta-king-real.com ("we," "us," or "our"), concerning your access to and use of the Services. By accessing the Services, you agree that you have read, understood, and agree to be bound by these Terms. If you do not agree, you are prohibited from using the Services and must discontinue use immediately.

2. DESCRIPTION OF THE SERVICES

The services provided (collectively, "the Services") consist of an independent media platform and informational archive. Our function is to:

You acknowledge and agree to the following:

3. PRIVACY POLICY

Our Privacy Policy, which is incorporated into these Terms, describes how we handle the information you may provide to us. By using the Services, you consent to the collection and use of this information as set forth in the Privacy Policy.

4. USER ELIGIBILITY AND RESPONSIBILITY

The Services are intended for users who are of the legal age of majority in their jurisdiction. It is your sole and absolute responsibility to ensure that your access to and use of the Services is not in violation of any applicable local, state, or national law or regulation in your jurisdiction.

5. GOVERNING LAW AND JURISDICTION

These Terms and any dispute that arises between you and us will be governed by the laws of the State of Wyoming, USA, without regard to its conflict of law principles. You agree that all disputes related to these Terms or the Services will be brought exclusively in the state and federal courts located in Cheyenne, Wyoming, USA. You hereby consent to the personal jurisdiction and venue in such forums.

6. INTELLECTUAL PROPERTY RIGHTS

The Services and their original content, features, and functionality are and will remain the exclusive property of us and our licensors, protected by copyright, trademark, and other laws of the United States and foreign countries.

7. PROHIBITED CONDUCT

You agree not to misuse the Services, including but not limited to, scraping, interfering with the network, or using the data for any commercial purpose without our prior written consent.

8. DISCLAIMERS; LIMITATION OF LIABILITY

THE SERVICES ARE PROVIDED "AS-IS" AND "AS-AVAILABLE" AT YOUR SOLE RISK. TO THE MAXIMUM EXTENT PERMITTED BY LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.

LIMITATION OF LIABILITY: IN NO EVENT SHALL WE BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES. OUR AGGREGATE LIABILITY SHALL NOT EXCEED ONE HUNDRED U.S. DOLLARS (U.S. $100.00).

9. GENERAL TERMS

', ], 'privacy' => [ 'title' => 'Privacy Policy', 'description' => 'Privacy Policy for Satta King Real — how we handle information, cookies, advertising, and third-party services.', 'keywords' => 'satta king privacy policy, satta king real privacy', 'robots' => 'index/follow', 'ads_h1' => 'Privacy Policy', 'content' => '

Effective Date: September 14, 2026

1. INFORMATION WE COLLECT

We collect minimal information from visitors. The Website is a non-transactional informational portal. We do not require registration to view content.

2. COOKIES AND ADVERTISING

We may use third-party advertising partners (such as Google AdSense) and analytics services (such as Google Analytics/GTM). These vendors may use cookies or similar technologies to serve ads and measure interactions. You can opt out of personalised advertising via Google Ads Settings.

3. THIRD-PARTY LINKS

Our pages may contain links to external websites. We are not responsible for the privacy practices of those websites. We encourage you to review their privacy policies.

4. DATA SECURITY

We take reasonable measures to protect any information collected. However, no method of transmission over the Internet is 100% secure, and we cannot guarantee absolute security.

5. CHILDREN\'S PRIVACY

The Services are intended for users who are of legal age in their jurisdiction. We do not knowingly collect personal information from minors.

6. CHANGES TO THIS POLICY

We may update this Privacy Policy from time to time. Changes will be posted on this page with an updated effective date.

7. CONTACT US

If you have questions about this Privacy Policy, please contact us through the contact form available on the Website.

', ], ]; } function load_seo(): array { $data = data_read(SEO_FILE); if (!is_array($data)) return seo_defaults(); $defaults = seo_defaults(); foreach ($defaults as $page => $fields) { if (!isset($data[$page]) || !is_array($data[$page])) { $data[$page] = $fields; continue; } foreach ($fields as $k => $v) { if (!isset($data[$page][$k]) || !is_string($data[$page][$k])) { $data[$page][$k] = $v; } } } return $data; } function save_seo(array $data): bool { return data_write(SEO_FILE, $data); } function faq_defaults(): array { return [ ['q' => 'Q. WHAT IS SATTA KING (SATTA MATKA)?', 'a' => 'A. Satta King, originally Satta Matka, is a number-based game that began in India in the 1960s. Today, the term mostly refers to online Satta King Fast results, charts, and historical Matka data shared for educational purposes.'], ['q' => 'Q. What is Satta King?', 'a' => 'A. Satta King Fast Result website ek informational application hai jahan users daily fast satta result updates aur number charts dekh sakte hain.'], ['q' => 'Q. क्या मोबाइल पर सट्टा रिजल्ट तेजी से देखा जा सकता है?', 'a' => 'A. हमारी वेबसाइट में हमारे उपयोगकर्ता सट्टा परिणाम सुपर फास्ट प्राप्त कर सकते हैं जो आपको अपने मोबाइल में तेजी से सट्टा परिणाम प्राप्त करने में मदद करते हैं।'], ['q' => 'Q. Satta King Fast par results kitni der me update hote hain?', 'a' => 'A. Satta King Fast Result website par results super fast update hote hain. Jaise hi official result aata hai, wo board me turant show ho jata hai. Aapko kahi aur jaane ki zaroorat nahi — ek hi jagah sab mil jayega.'], ['q' => 'Q. Kaunse games ke results milte hain yahan?', 'a' => 'A. Yahan Delhi Bazar, Shri Ganesh, Faridabad, Ghaziabad, Gali, Desawar, Noida King, Dehradun, Meerut City, Agra Bazar, Mahalaxmi, Uttarakhand aur 100+ se zyada games ke daily fast results aur charts available hain.'], ['q' => 'Q. Chart kaise dekhein?', 'a' => 'A. Homepage par hi "Monthly Satta King Result Chart" section me jaake koi bhi month aur year select karein aur "Go" dabayein. Aapko Delhi Bazar, Shri Ganesh, Faridabad, Ghaziabad, Gali, Desawar sabka combined chart ek saath mil jayega.'], ['q' => 'Q. Kya mobile par results dekh sakte hain?', 'a' => 'A. Haan, hamari website mobile-friendly hai. Aap apne phone par kisi bhi browser me khole aur turant fast satta results dekhein. Koi app download karne ki zaroorat nahi — sirf link open karein.'], ['q' => 'Q. Record / purane results kaise dekhein?', 'a' => 'A. Har game ka apna record page hai. Game ke naam ya result row par click karein — aapko uss game ka poora record (past results) dikh jayega. Charts section me bhi mahine ke hisaab se poora data available hai.'], ['q' => 'Q. Kya ye website sirf educational purposes ke liye hai?', 'a' => 'A. Haan, hamari website sirf informational aur educational purposes ke liye hai. Yahan daily results, charts, aur historical data sirf reference ke liye share kiya jata hai. Hamari team fast aur accurate result updates provide karti hai.'], ]; } function load_faq(): array { $data = data_read(FAQ_FILE); if (!is_array($data)) return faq_defaults(); $items = []; foreach ($data as $row) { if (!is_array($row) || !isset($row['q']) || !isset($row['a'])) continue; $items[] = [ 'q' => (string)$row['q'], 'a' => (string)$row['a'], ]; } return $items ?: faq_defaults(); } function save_faq(array $items): bool { return data_write(FAQ_FILE, $items); } /* ---------- Site integration settings (analytics, ads, custom code) ---------- */ function settings_defaults(): array { return [ 'ga_measurement_id' => '', 'gtm_container_id' => '', 'ads_home_top' => '', 'ads_home_mid' => '', 'ads_home_bottom' => '', 'custom_head' => '', 'custom_body' => '', ]; } function load_settings(): array { $data = data_read(SETTINGS_FILE); if (!is_array($data)) return settings_defaults(); $defaults = settings_defaults(); foreach ($defaults as $k => $v) { if (!isset($data[$k]) || !is_string($data[$k])) { $data[$k] = $v; } } return $data; } function save_settings(array $data): bool { return data_write(SETTINGS_FILE, $data); } /* ---------- Telegram Bot config (admin-editable; read by scripts/skj-sync-php.php) ---------- */ define('TELEGRAM_CONFIG_FILE', DATA_DIR . '/telegram-config.json'); function telegram_config_defaults(): array { return [ 'enabled' => false, 'token' => '', 'chat_id' => '', 'post_games' => [], // empty = ALL games broadcast 'groups' => [], // [{id,title,active,games[],added,role}] — multi-group broadcast 'last_scan' => 0, // timestamp of last successful Scan New Groups run 'verify_cursor' => 0, // rate-limit ke liye verify cursor (0 = shuru se) ]; } function load_telegram_config(): array { $data = data_read(TELEGRAM_CONFIG_FILE); if (!is_array($data)) { $data = telegram_config_defaults(); // Seeding: agar naya config file exist nahi karta, purane data/.telegram.php // ke values as-is chadhwao taaki admin ko kuch break na lage. $legacy = DATA_DIR . '/.telegram.php'; if (file_exists($legacy)) { include $legacy; $data['enabled'] = ($TG_TOKEN !== '' && $TG_CHAT_ID !== ''); $data['token'] = (string)($TG_TOKEN ?? ''); $data['chat_id'] = (string)($TG_CHAT_ID ?? ''); } } $defaults = telegram_config_defaults(); // Type-reconcile karo, value phenko mat. // Pehle yahan `gettype()` compare tha aur default assign hota tha — jisse // `enabled` ko 1 (int) likh diya jaaye to wo har load par FALSE ho jata tha // (bool default vs int value) aur bot chup-chaup band ho jata tha, bina // kisi error ke. Ab mismatch par value cast hoti hai, zero nahi. foreach ($defaults as $k => $v) { if (!array_key_exists($k, $data) || $data[$k] === null) { $data[$k] = $v; continue; } if (is_bool($v)) { $data[$k] = (bool)$data[$k]; continue; } if (is_array($v)) { if (!is_array($data[$k])) $data[$k] = $v; continue; } if (is_int($v) && !is_int($data[$k])) { $data[$k] = (int)$data[$k]; continue; } if (is_string($v)&& !is_string($data[$k])) { $data[$k] = (string)$data[$k]; continue; } } $data['groups'] = telegram_normalize_groups($data); return $data; } function save_telegram_config(array $data): bool { $data['groups'] = telegram_normalize_groups($data); return data_write(TELEGRAM_CONFIG_FILE, $data); } define('TELEGRAM_CONFIG_LOCK', DATA_DIR . '/.tg-config.lock'); /** * Locked read-modify-write on telegram-config.json. * * Cron har ~10 min me groups list update karta hai aur admin panel bhi usi * file ko likhta hai. Bina lock ke dono ke beech me "read → modify → write" * race ho sakta hai: cron naye group add kare, panel usi waqt save kare to * cron ka naya group gayab ho jaata hai (ya ulta). * * $mutator ko fresh config milta hai aur use [newConfig, result] return karna * chahiye. Config lock ke andar hi read hota hai aur write bhi usi ke andar * hota hai, isliye dono writers kabhi ek doosre ko overwrite nahi kar sakte. * * @return array $mutator ka result (ya ['ok'=>true] agar usne kuch nahi diya) */ function telegram_config_mutate(callable $mutator): array { $fp = @fopen(TELEGRAM_CONFIG_LOCK, 'c'); if ($fp) @chmod(TELEGRAM_CONFIG_LOCK, 0666); $locked = false; if ($fp) { $locked = @flock($fp, LOCK_EX); if (!$locked) { @fclose($fp); $fp = null; } } try { $cfg = load_telegram_config(); // fresh read, lock ke andar $res = $mutator($cfg); if (is_array($res) && isset($res[0]) && is_array($res[0])) { // SAFETY: mutator "kuch nahi badla" batane ke liye [[], $result] // return kar sakta hai. Usko save karna config ko poori tarah // mita deta tha (token + saare groups gayab) — isliye khaali config // kabhi nahi likhi jaati, balki mutator ko apni original config // wapas bhejni padti hai. if (empty($res[0]) && !empty($cfg)) { return isset($res[1]) && is_array($res[1]) ? $res[1] : ['ok' => true]; } save_telegram_config($res[0]); return isset($res[1]) && is_array($res[1]) ? $res[1] : ['ok' => true]; } return is_array($res) ? $res : ['ok' => true]; } finally { if ($fp) { @flock($fp, LOCK_UN); @fclose($fp); } } } /** Non-blocking config lock — jahan caller khud lock hold karke kaam kare. */ function telegram_config_lock_try(): bool { $fp = @fopen(TELEGRAM_CONFIG_LOCK, 'c'); if (!$fp) return false; if (!@chmod(TELEGRAM_CONFIG_LOCK, 0666)) { /* best effort */ } if (!@flock($fp, LOCK_EX | LOCK_NB)) { @fclose($fp); return false; } $GLOBALS['__tg_cfg_lock_fp'] = $fp; return true; } function telegram_config_lock_release(): void { if (!empty($GLOBALS['__tg_cfg_lock_fp']) && is_resource($GLOBALS['__tg_cfg_lock_fp'])) { @flock($GLOBALS['__tg_cfg_lock_fp'], LOCK_UN); @fclose($GLOBALS['__tg_cfg_lock_fp']); } $GLOBALS['__tg_cfg_lock_fp'] = null; } /* ---------- multi-group engine ---------- * Bot jis bhi group me add kiya gaya (admin ya member) uska chat id Telegram * ke "my_chat_member" update se auto-register ho jata hai, aur result har active * group me broadcast hota hai. Jo group bot ko hata de usme wo deactivate ho * jata hai. Manual add bhi supported hai — Telegram sirf ~24 ghante purane * updates deta hai, isliye purane groups scan nahi honge. */ define('TELEGRAM_OFFSET_FILE', DATA_DIR . '/tg-offset.json'); define('TELEGRAM_SENT_FILE', DATA_DIR . '/tg-sent.json'); define('TELEGRAM_RUNTIME_FILE', DATA_DIR . '/tg-runtime.json'); /** * tg-sent.json ki keys "CODE/YYYY-MM-DD" aur "G::CODE/YYYY-MM-DD" hoti * hain. Ye file pehle kabhi prune nahi hui — matlab har din har game har group * ke liye ek entry, forever. Saalon me ye kai MB ho jati aur har page load par * poori file read + json_decode hoti. Ab 45 din se purani entries hata dete hain. * (45 din ka buffer safe hai — result wapas 1-2 din me aata hai.) */ function telegram_prune_sent(int $keepDays = 45): array { $stats = ['before' => 0, 'after' => 0, 'pruned' => 0]; if (!is_file(TELEGRAM_SENT_FILE)) return $stats; $raw = json_decode((string)@file_get_contents(TELEGRAM_SENT_FILE), true); if (!is_array($raw)) return $stats; $stats['before'] = count($raw); $cutoff = date('Y-m-d', time() - ($keepDays * 86400)); $keep = []; foreach ($raw as $k => $v) { $ks = (string)$k; // Date key 'YYYY-MM-DD' hai, usko se slice karo (prefix 'G:...' ho sakta hai). if (preg_match('/(\d{4}-\d{2}-\d{2})/', $ks, $m) && $m[1] < $cutoff) continue; $keep[$k] = $v; } $stats['after'] = count($keep); $stats['pruned'] = $stats['before'] - $stats['after']; if ($stats['pruned'] > 0) { $json = json_encode($keep, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if (is_string($json)) { $tmp = @tempnam(DATA_DIR, '.tmp_sent_'); if ($tmp !== false && @file_put_contents($tmp, $json) !== false) { @chmod($tmp, 0666); if (!@rename($tmp, TELEGRAM_SENT_FILE)) @unlink($tmp); } else { if ($tmp !== false) @unlink($tmp); @file_put_contents(TELEGRAM_SENT_FILE, $json, LOCK_EX); } @chmod(TELEGRAM_SENT_FILE, 0666); } } return $stats; } /** Chhota runtime status file — cron isme "last broadcast" likhta hai taaki * admin panel pata kar sake ki posting abhi bhi chal rahi hai ya nahi. */ function telegram_runtime_get(): array { $d = data_read(TELEGRAM_RUNTIME_FILE); return is_array($d) ? $d : []; } function telegram_runtime_set(string $k, $v): void { $d = telegram_runtime_get(); $d[$k] = $v; $d['at'] = time(); data_write(TELEGRAM_RUNTIME_FILE, $d); } /** Clean + de-dupe the groups[] list, and fold the legacy single chat_id in. */ function telegram_normalize_groups(array $cfg): array { $out = []; $seen = []; $raw = $cfg['groups'] ?? []; if (is_array($raw)) { foreach ($raw as $g) { if (!is_array($g)) continue; $id = trim((string)($g['id'] ?? ($g['chat_id'] ?? ''))); if ($id === '' || isset($seen[$id])) continue; $games = $g['games'] ?? []; $games = is_array($games) ? array_values(array_filter(array_map('strval', $games))) : array_values(array_filter(array_map('strval', preg_split('/[\s,]+/', (string)$games)))); $out[] = [ 'id' => $id, 'title' => trim((string)($g['title'] ?? '')) ?: ('Group ' . $id), 'active' => !empty($g['active']) ? 1 : 0, 'games' => $games, // empty = follow the global post_games list 'added' => (int)($g['added'] ?? 0), 'role' => (string)($g['role'] ?? 'member'), ]; $seen[$id] = true; } } // Legacy single "chat_id" ko groups list me migrate kar do (non-destructive): // purana setup bina kuch kiye hi multi-group mode me kaam karega. $legacy = trim((string)($cfg['chat_id'] ?? '')); if ($legacy !== '' && !isset($seen[$legacy])) { $out[] = [ 'id' => $legacy, 'title' => 'Primary group (legacy)', 'active' => 1, 'games' => [], 'added' => time(), 'role' => 'legacy', ]; } return $out; } /** [chatId => group row] for every group that should receive broadcasts. */ function telegram_active_chats(array $cfg): array { $out = []; foreach (telegram_normalize_groups($cfg) as $g) { if (empty($g['active'])) continue; $out[$g['id']] = $g; } return $out; } /** * Generic Bot API caller — ['ok'=>bool,'desc'=>string,'result'=>mixed]. * $method me query string allowed hai (jaise 'getUpdates?limit=10'). * Telegram 4xx pe bhi body padhta hai, warna asli reason ("chat not found", * "bot was kicked by the user") chhup jata hai aur sirf "network error" dikhta hai. * * Transport-level failure (connection reset / DNS / http 0) par do baar retry * karta hai — shared hosting par file_get_contents kabhi-kabhi bina bade kaaran * ka false return kar deta hai, jisse ek hi valid group "fail" dikh jaata hai. */ function telegram_call(string $token, string $method, array $params = [], int $attempts = 3): array { if ($token === '' || $method === '') { return ['ok' => false, 'desc' => 'empty token / method', 'result' => null]; } $url = 'https://api.telegram.org/bot' . $token . '/' . ltrim($method, '/'); $ctx = null; if ($params) { $ctx = stream_context_create(['http' => [ 'method' => 'POST', 'header' => "Content-Type: application/x-www-form-urlencoded\r\n", 'content' => http_build_query($params), 'timeout' => 20, 'ignore_errors' => true, ]]); } $code = 0; $out = false; $j = null; $tries = max(1, $attempts); for ($i = 1; $i <= $tries; $i++) { telegram_throttle(); $out = @file_get_contents($url, false, $ctx); $hdrs = function_exists('http_get_last_response_headers') ? http_get_last_response_headers() : []; $code = 0; foreach ((array)$hdrs as $h) { if (preg_match('#^HTTP/\S+\s+(\d{3})#', (string)$h, $m)) { $code = (int)$m[1]; break; } } // Response mil gaya (chahe 4xx hi kyun na ho) — transport retry ka ab // koi matlab nahi. Neeche sirf 429 (rate limit) pe retry hoga. if ($out === false) { if ($i < $tries) usleep(400000 * $i); continue; } $j = json_decode($out, true); if (!is_array($j)) return ['ok' => false, 'desc' => 'bad response (http ' . $code . ')', 'result' => null]; if (!empty($j['ok'])) return ['ok' => true, 'desc' => 'ok', 'result' => $j['result'] ?? null]; $errCode = (int)($j['error_code'] ?? 0); // 429 = rate limited. Telegram retry_after batata hai — uske hisaab se // rukein aur dobara try karein. Bina iske 25-group verify me kai group // "fail" ho jate the aur admin ko lagta tha group kharab hai. if ($errCode === 429 && $i < $tries) { $retryAfter = (int)(is_array($j['parameters'] ?? null) ? ($j['parameters']['retry_after'] ?? 1) : 1); $retryAfter = max(1, min(30, $retryAfter)); telegram_throttle_notes('429 — ' . $retryAfter . 's ruka'); usleep($retryAfter * 1000000); continue; } $desc = (string)($j['description'] ?? 'unknown error'); if (isset($j['error_code'])) $desc = 'http ' . $j['error_code'] . ': ' . $desc; return ['ok' => false, 'desc' => $desc, 'result' => null]; } if ($out === false) return ['ok' => false, 'desc' => 'network error (http ' . $code . ')', 'result' => null]; return ['ok' => false, 'desc' => 'rate limited (429) — thodi der baad try karein', 'result' => null]; } /** Chhoti si note file — taaki admin "Scan" par dekh sake ki 429 aaya tha. */ function telegram_throttle_notes(string $note): void { $f = DATA_DIR . '/.tg-notes.log'; @file_put_contents($f, '[' . date('d-m H:i:s') . '] ' . $note . "\n", FILE_APPEND | LOCK_EX); @chmod($f, 0666); // Notes file unbounded na bane. if (is_file($f) && filesize($f) > 65536) { $lines = @file($f, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; @file_put_contents($f, implode("\n", array_slice($lines, -200)) . "\n", LOCK_EX); } } /** * Simple on-disk throttle — Telegram ka limit ~30 req/min hai. "Scan & Verify" * ek run me 25+ call karta hai, aur page load par diagnostics bhi 2-3 karta hai. * Bina throttle ke ek saath 30 cross karte hi 429 aata hai aur adhoora kaam * ho jata hai. Har call se pehle minimum gap rakha jata hai. */ function telegram_throttle(float $minGap = 0.09): void { static $last = null; $f = DATA_DIR . '/.tg-throttle'; $now = microtime(true); $prev = 0.0; if (is_file($f)) $prev = (float)@file_get_contents($f); if ($prev > 0) { $wait = ($prev + $minGap) - $now; if ($wait > 0) usleep((int)($wait * 1000000)); } @file_put_contents($f, (string)microtime(true)); $last = $now; } /** * Bot API getUpdates — returns ['ok'=>bool,'desc'=>string,'result'=>array]. */ function telegram_get_updates(string $token, int $offset = 0, int $limit = 100): array { $q = [ 'timeout' => 0, 'limit' => $limit, 'allowed_updates' => json_encode(['my_chat_member', 'chat_member']), ]; if ($offset > 0) $q['offset'] = $offset; $r = telegram_call($token, 'getUpdates?' . http_build_query($q)); $r['result'] = is_array($r['result']) ? $r['result'] : []; return $r; } /** * Chat ID ya @username se group resolve karo (getChat). * Admin ko numeric ID dhundhne ki zaroorat nahi — seedha group link ya * @username daalna kaafi hai. Link formats accepted: * -1001234567890 | @mygroup | mygroup | https://t.me/mygroup | t.me/+invite * Returns ['ok'=>bool,'desc'=>string,'id'=>string,'title'=>string,'type'=>string]. */ function telegram_get_chat(string $token, string $ref): array { $fail = ['ok' => false, 'desc' => 'kuch galat', 'id' => '', 'title' => '', 'type' => '']; $ref = trim($ref); if ($ref === '') return ['desc' => 'ID ya @username khaali hai'] + $fail; if (stripos($ref, 't.me/') !== false) { if (!preg_match('#t\.me/([+A-Za-z0-9_]+)#', $ref, $m)) { return ['desc' => 'link samajh nahi aaya'] + $fail; } $ref = ($m[1][0] === '+') ? substr($m[1], 1) : '@' . $m[1]; } $ref = ltrim(trim($ref), '@'); if ($ref === '') return ['desc' => 'ID ya @username khaali hai'] + $fail; $isNumeric = (bool)preg_match('/^-?\d+$/', $ref); $r = telegram_call($token, 'getChat', ['chat_id' => $isNumeric ? $ref : '@' . $ref]); if (!$r['ok']) { $d = $r['desc']; if (stripos($d, 'not found') !== false) { $d = is_numeric($ref) || $isNumeric ? 'Ye Chat ID galat lag raha hai (group me bot ko add karo, phir dobara try karo)' : 'Ye @username galat ya private hai — public group ka link use karo'; } return ['desc' => $d] + $fail; } $c = is_array($r['result']) ? $r['result'] : []; $id = trim((string)($c['id'] ?? '')); if ($id === '') return ['desc' => 'Telegram ne khali chat id diya'] + $fail; return [ 'ok' => true, 'desc' => 'ok', 'id' => $id, 'title' => trim((string)($c['title'] ?? ($c['username'] ?? ''))) ?: ('Group ' . $id), 'type' => (string)($c['type'] ?? ''), ]; } /** * getWebhookInfo — webhook set hone pe getUpdates 403 de deta hai, jisse * "Scan New Groups" chup-chaap kuch nahi karta. Isiliye har scan se pehle * ye check karke user ko saaf error + fix dikhate hain. */ function telegram_webhook_info(string $token): array { $r = telegram_call($token, 'getWebhookInfo'); if (!$r['ok']) { return ['ok' => false, 'desc' => $r['desc'], 'url' => '', 'pending' => 0, 'error' => '']; } $i = is_array($r['result']) ? $r['result'] : []; return [ 'ok' => true, 'desc' => 'ok', 'url' => trim((string)($i['url'] ?? '')), 'pending' => (int)($i['pending_update_count'] ?? 0), 'error' => trim((string)($i['last_error_message'] ?? '')), ]; } /** * Read-only health snapshot for the admin panel header. Never writes config. * * NOTE: ye 2-3 Telegram API calls karta hai (getMe + getWebhookInfo). Pehle ye * har page load par bina cache chal raha tha, aur saath me section me ek alag * telegram_get_info() bhi duplicate getMa maar raha tha — yani ek page load = * 3 API calls. Shared hosting par page slow hota tha aur Telegram ka 30/min * limit jaldi khatam ho jata tha. Ab 90 second ka cache hai; force=true se * turant fresh check (Scan button aise hi karta hai). */ function telegram_diagnostics(string $token, array $cfg, array $opts = []): array { $groups = telegram_normalize_groups($cfg); $active = 0; foreach ($groups as $g) if (!empty($g['active'])) $active++; $d = [ 'token_ok' => false, 'bot_username' => '', 'webhook_url' => '', 'webhook_set' => false, 'pending' => 0, 'webhook_error' => '', 'groups' => count($groups), 'active' => $active, 'last_scan' => (int)($cfg['last_scan'] ?? 0), 'errors' => [], 'checked_at' => 0, 'cached' => false, ]; if ($token === '') { $d['errors'][] = 'Bot token save nahi hua'; return $d; } // ---- cache ---- $cacheFile = DATA_DIR . '/.tg-diag.json'; $ttl = max(0, (int)($opts['ttl'] ?? 90)); $force = !empty($opts['force']); if (!$force && $ttl > 0 && is_file($cacheFile)) { $c = json_decode((string)@file_get_contents($cacheFile), true); // Token badal gaya ho to cache bekaar hai. if (is_array($c) && ($c['token'] ?? '') === $token && isset($c['at']) && (time() - (int)$c['at']) < $ttl && is_array($c['data'] ?? null)) { $cached = $c['data']; $cached['groups'] = $d['groups']; $cached['active'] = $d['active']; $cached['last_scan'] = $d['last_scan']; $cached['checked_at'] = (int)$c['at']; $cached['cached'] = true; return $cached; } } $me = telegram_call($token, 'getMe'); if ($me['ok']) { $d['token_ok'] = true; $d['bot_username'] = (string)((is_array($me['result']) ? ($me['result']['username'] ?? '') : '')); } else { $d['errors'][] = 'Token invalid: ' . $me['desc']; } $wh = telegram_webhook_info($token); if ($wh['ok']) { $d['webhook_url'] = $wh['url']; $d['webhook_set'] = ($wh['url'] !== ''); $d['pending'] = $wh['pending']; $d['webhook_error'] = $wh['error']; if ($d['webhook_set']) { $d['errors'][] = 'Webhook set hai (' . $wh['url'] . ') — isliye getUpdates kaam nahi kar raha. @BotFather → /deletewebhook chalao.'; } } else { $d['errors'][] = 'Webhook check fail: ' . $wh['desc']; } $d['checked_at'] = time(); if ($ttl > 0) { @file_put_contents($cacheFile, json_encode( ['token' => $token, 'at' => $d['checked_at'], 'data' => $d], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), LOCK_EX); @chmod($cacheFile, 0666); } return $d; } /** Cache turpat refresh karo (Scan / Save ke baad). */ function telegram_diag_invalidate(): void { @unlink(DATA_DIR . '/.tg-diag.json'); } /** * Group discovery — do phases: * * Phase 1 (discover): getUpdates se pending my_chat_member updates padho aur * naye group register karo, hataye hue deactivate karo. * * Phase 2 (verify): yehi button har jaane par har registered group ko * getChat se verify karta hai — title refresh, aur agar bot group se * nikal chuka ho to turant deactivate + report. * * Phase 2 isliye zaroori hai kyunki getUpdates sirf ~24 ghante tak ke pending * updates deta hai aur cron har ~10 min me offset aage badha deta hai. Isliye * sirf getUpdates par depend karein to button hamesha "kuch nahi mila" deta. * Verify se button ka kaam deterministic ho jaata hai. * * Returns: * ok, desc, groups, added[], removed[], retitled[id=>newTitle], * unreachable[id=>reason], scanned, report[] (human readable lines) */ function telegram_discover_groups(string $token, array $cfg, array $opts = []): array { $verify = !array_key_exists('verify', $opts) || !empty($opts['verify']); $groups = telegram_normalize_groups($cfg); $byId = []; foreach ($groups as $g) $byId[$g['id']] = $g; $offset = 0; if (file_exists(TELEGRAM_OFFSET_FILE)) { $o = json_decode((string)@file_get_contents(TELEGRAM_OFFSET_FILE), true); if (is_array($o)) $offset = (int)($o['offset'] ?? 0); } $report = []; $added = []; $removed = []; $retitled = []; $unreach = []; $r = telegram_get_updates($token, $offset); if (!$r['ok']) { return [ 'ok' => false, 'desc' => $r['desc'], 'groups' => $groups, 'added' => [], 'removed' => [], 'retitled' => [], 'unreachable' => [], 'scanned' => 0, 'report' => ['Scan fail: ' . $r['desc']], ]; } // ---- Phase 1: pending updates se naye group ---- $maxOffset = $offset; foreach ($r['result'] as $u) { $uid = (int)($u['update_id'] ?? 0); if ($uid >= $maxOffset) $maxOffset = $uid + 1; $mc = $u['my_chat_member'] ?? ($u['chat_member'] ?? null); if (!is_array($mc)) continue; $chat = $mc['chat'] ?? null; if (!is_array($chat)) continue; $id = trim((string)($chat['id'] ?? '')); $type = (string)($chat['type'] ?? ''); if ($id === '' || !in_array($type, ['group', 'supergroup'], true)) continue; $title = trim((string)($chat['title'] ?? ($chat['username'] ?? ''))); if ($title === '') $title = 'Group ' . $id; $status = (string)($mc['new_chat_member']['status'] ?? ($mc['old_chat_member']['status'] ?? '')); if (in_array($status, ['left', 'kicked'], true)) { if (isset($byId[$id]) && !empty($byId[$id]['active'])) { $byId[$id]['active'] = 0; $removed[] = $id; } continue; } if (in_array($status, ['administrator', 'creator', 'member'], true)) { if (!isset($byId[$id])) { $byId[$id] = [ 'id' => $id, 'title' => $title, 'active' => 1, 'games' => [], 'added' => time(), 'role' => $status, ]; $added[] = $id; } else { if ($byId[$id]['title'] !== $title) { $byId[$id]['title'] = $title; $retitled[$id] = $title; } if (in_array($status, ['administrator', 'creator'], true)) $byId[$id]['active'] = 1; $byId[$id]['role'] = $status; } } } // ---- Phase 2: har registered group ko getChat se verify karo ---- // Telegram ka rate limit ~30 req/min hai. Saare groups ek saath check karne // par 30+ groups hone se verify half-fail hota tha, isliye har run me // limited group verify hote hain aur cursor aage badhta rehta hai — agli // baar "Scan" dabane par agle group verify honge. Isse group count kitna // bhi ho, har run me API ke andar hi rehta hai. $verifyCap = max(1, (int)($opts['verify_limit'] ?? 25)); $cursor = (int)($cfg['verify_cursor'] ?? 0); $ids = array_keys($byId); $queue = []; if ($ids) { $total = count($ids); for ($k = 0; $k < $total; $k++) { $queue[] = $ids[($cursor + $k) % $total]; } } $todo = array_slice($queue, 0, $verifyCap); $cursor = $ids ? ((($cursor + count($todo)) % count($ids))) : 0; if ($verify && $todo) { foreach ($todo as $id) { $c = telegram_get_chat($token, (string)$id); if (!$c['ok']) { $unreach[$id] = $c['desc']; continue; } $nt = trim((string)($c['title'] ?? '')); if ($nt !== '' && $nt !== $byId[$id]['title']) { $byId[$id]['title'] = $nt; $retitled[$id] = $nt; } if (!in_array((string)$c['type'], ['group', 'supergroup'], true)) { $unreach[$id] = 'ye group nahi lagta (' . $c['type'] . ')'; } } } $groups = array_values($byId); if ($maxOffset > $offset) { @file_put_contents(TELEGRAM_OFFSET_FILE, json_encode(['offset' => $maxOffset, 'at' => time()]), LOCK_EX); @chmod(TELEGRAM_OFFSET_FILE, 0666); } // Config likhte waqt sidecar lock pakadna ZAROORI hai. Ye function cron bhi // call karta hai aur admin bhi — dono ka apna apna lock hota tha, isliye // scan ke beech me admin ka "Add group" save pichhne se overwrite ho jata. // Yahan lock andar hi lete hain: agar bahar se koi pehle se pakda hai to // turant return kar dete hain (dobara try karna, na ki bina lock likhna). if (!telegram_config_lock_try()) { return [ 'ok' => false, 'desc' => 'config abhi kisi aur write me hai — 1 baar phir try karo', 'groups' => $groups, 'added' => $added, 'removed' => $removed, 'retitled' => $retitled, 'unreachable' => $unreach, 'scanned' => count($r['result']), 'report' => ['⚠️ Config lock busy tha, scan save nahi hua. Phir se dabao.'], ]; } try { // Lock ke andar fresh config padho — tabhi se latest groups mile honge. $fresh = load_telegram_config(); $fresh['groups'] = $groups; $fresh['last_scan'] = time(); $fresh['verify_cursor'] = $cursor; save_telegram_config($fresh); } finally { telegram_config_lock_release(); } telegram_diag_invalidate(); // ---- readable report ---- $wh = telegram_webhook_info($token); if ($wh['ok'] && $wh['url'] !== '') { $report[] = '⚠️ Webhook set hai (' . $wh['url'] . ') — naye groups auto-detect NAHI honge. @BotFather → /deletewebhook chalao.'; } $report[] = 'Pending updates check kiye: ' . count($r['result']); if ($added) foreach ($added as $id) $report[] = '✅ Naya group add: ' . ($byId[$id]['title'] ?? $id); if ($retitled) foreach ($retitled as $id => $t) $report[] = '✏️ Naam badla: ' . $t; if ($removed) foreach ($removed as $id) $report[] = '⛔ Bot nikal gaya, deactivate: ' . ($byId[$id]['title'] ?? $id); if ($unreach) foreach ($unreach as $id => $why) $report[] = '❌ Verify fail (' . $id . '): ' . $why; if (!$added && !$removed && !$retitled && !$unreach) { $report[] = 'Sab check ho gaya — koi naya ya khatam group nahi mila.'; } $pendingVerify = count($ids) - count($todo); if ($verify && $pendingVerify > 0) { $report[] = 'ℹ️ Verify ' . count($todo) . '/' . count($ids) . ' group kiya (Telegram rate limit). ' . $pendingVerify . ' baaki agli "Scan" me verify honge.'; } return [ 'ok' => true, 'desc' => 'ok', 'groups' => $groups, 'added' => $added, 'removed' => $removed, 'retitled' => $retitled, 'unreachable' => $unreach, 'scanned' => count($r['result']), 'report' => $report, ]; } /** Send one message to many chats — returns [chatId => ['ok'=>bool,'desc'=>string]]. * Thoda gap rakha jata hai kyunki 30+ groups ek saath bhejne par Telegram * 429 de deta tha aur aadhi messages chhoot jati thin. */ function telegram_broadcast(string $token, array $chatIds, string $text, float $gap = 0.12): array { $out = []; $n = 0; foreach ($chatIds as $id) { if ($n > 0 && $gap > 0) usleep((int)($gap * 1000000)); $n++; $out[(string)$id] = telegram_send_message($token, (string)$id, $text); } return $out; } /** Telegram Bot API getMe — retries wale shared helper se, taaki transient * network hiccup par panel galat "BOT ? / token invalid" na dikhaye. */ function telegram_get_info(string $token): ?array { if ($token === '') return null; $r = telegram_call($token, 'getMe'); if (!$r['ok'] || !is_array($r['result'])) return null; return $r['result']; } /** Telegram Bot API sendMessage — returns ['ok'=>bool, 'desc'=>string]. */ function telegram_send_message(string $token, string $chatId, string $text): array { if ($token === '' || $chatId === '') return ['ok' => false, 'desc' => 'empty token / chat id']; $r = telegram_call($token, 'sendMessage', [ 'chat_id' => $chatId, 'text' => $text, 'parse_mode' => 'HTML', 'disable_web_page_preview' => 'true', ]); return ['ok' => (bool)$r['ok'], 'desc' => (string)$r['desc']]; } /** Echo the gtag.js block when a GA4 measurement ID is configured (used in ). */ function render_analytics_head(): void { $s = load_settings(); $ga = trim($s['ga_measurement_id'] ?? ''); if ($ga === '') return; echo " \n"; echo ' ' . "\n"; echo " \n"; } /** Echo the GTM noscript iframe right after when a GTM container ID is configured. */ function render_analytics_body(): void { $s = load_settings(); $gtm = trim($s['gtm_container_id'] ?? ''); if ($gtm === '') return; echo " \n"; echo ' ' . "\n"; echo " \n"; } /** Echo the optional custom code when set. */ function render_custom_head(): void { $s = load_settings(); $code = trim($s['custom_head'] ?? ''); if ($code === '') return; echo $code . "\n"; } /** Echo the optional custom end-of- code when set. */ function render_custom_body(): void { $s = load_settings(); $code = trim($s['custom_body'] ?? ''); if ($code === '') return; echo $code . "\n"; } /** * Echo a responsive ad slot wrapper only when its code is configured. * Empty slots render nothing at all (no container, no whitespace). * Slots available: ads_home_top, ads_home_mid, ads_home_bottom. */ function render_ad_slot(string $slot): void { $s = load_settings(); $code = trim((string)($s[$slot] ?? '')); if ($code === '') return; echo '
' . "\n"; echo $code . "\n"; echo '
' . "\n"; } function seo_apply(string $template, array $vars): string { if ($template === '') return ''; $map = []; foreach ($vars as $k => $v) { $map['{' . $k . '}'] = (string)$v; } return strtr($template, $map); } /** * Sanitize admin-authored rich text to a safe HTML allowlist for public render. * Only: p, br, b, strong, i, em, u, s, strike, h2, h3, ul, ol, li, a[href=http/https/#//relative]. * Scripts, event handlers and style attributes are stripped. */ function sanitize_rich_content(?string $html): string { $html = (string)$html; if ($html === '') return ''; // rip out anything that can run code before the tag whitelist kicks in $html = preg_replace('/]*>.*?<\/script>\s*/is', '', $html); $html = preg_replace('/]*>.*?<\/style>\s*/is', '', $html); $html = preg_replace('/]*>.*?<\/iframe>\s*/is', '', $html); $html = preg_replace('/]*>.*?<\/object>\s*/is', '', $html); $html = preg_replace('/]*>/is', '', $html); $allow = '


    1. '; $html = strip_tags($html, $allow); // keep only whitelisted anchors with a safe href, drop every other attribute $html = preg_replace_callback('/]*)>(.*?)<\/a>/is', function (array $m): string { $href = ''; if (preg_match('/href\s*=\s*["\']([^"\']+)["\']/i', $m[1], $h)) { $url = trim($h[1]); if (preg_match('~^(https?://|/|#)~i', $url)) { $href = ' href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener"'; } } return '' . $m[2] . ''; }, $html); $html = preg_replace('/<(p|br|b|strong|i|em|u|s|strike|h2|h3|ul|ol|li)\b[^>]*>/i', '<$1>', $html); // keep only , strip every other span attribute $html = preg_replace_callback('/]*)>/i', function (array $m): string { return preg_match('/class\s*=\s*["\']highlight["\']/i', $m[1]) ? '' : ''; }, $html); $html = preg_replace('/<\/span>/i', '', $html); return $html; } function game_by_code(array $games, string $code): ?array { foreach ($games as $g) { if (($g['code'] ?? '') === $code) return $g; } return null; } function is_logged_in(): bool { return isset($_SESSION[SESSION_KEY]) && $_SESSION[SESSION_KEY] === true; } function current_user_name(): ?string { return isset($_SESSION[SESSION_USER]) ? $_SESSION[SESSION_USER] : null; } function is_admin_user(): bool { return ($_SESSION[SESSION_ROLE] ?? '') === 'admin'; } function user_game_code(): ?string { $code = $_SESSION[SESSION_GAME] ?? null; return $code !== '' ? $code : null; } function hash_password(string $plain): string { return password_hash($plain . PASSWORD_PEPPER, PASSWORD_DEFAULT); } /* ---------- TOTP (two-factor authentication) ---------- */ function base32_encode(string $data): string { $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; if ($data === '') return ''; $bin = ''; foreach (str_split($data) as $ch) $bin .= str_pad(decbin(ord($ch)), 8, '0', STR_PAD_LEFT); $out = ''; $len = strlen($bin); for ($i = 0; $i < $len; $i += 5) { $out .= $alphabet[bindec(str_pad(substr($bin, $i, 5), 5, '0', STR_PAD_RIGHT))]; } return $out; } function base32_decode(string $b32): string { $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; $b32 = strtoupper((string)$b32); $b32 = preg_replace('/[^A-Z2-7]/', '', $b32); $bits = ''; $len = strlen($b32); for ($i = 0; $i < $len; $i++) { $val = strpos($alphabet, $b32[$i]); if ($val === false) continue; $bits .= str_pad(decbin($val), 5, '0', STR_PAD_LEFT); } $bytes = ''; $bitsLen = strlen($bits); for ($i = 0; $i + 8 <= $bitsLen; $i += 8) { $bytes .= chr(bindec(substr($bits, $i, 8))); } return $bytes; } function totp_secret(): string { return base32_encode(random_bytes(16)); } function totp_code(string $secretBase32, ?int $at = null): string { if ($at === null) $at = time(); $counter = pack('N2', 0, intdiv($at, 30)); $hash = hash_hmac('sha1', $counter, base32_decode($secretBase32), true); $offset = ord($hash[strlen($hash) - 1]) & 0x0F; $binary = ((ord($hash[$offset]) & 0x7F) << 24) | ((ord($hash[$offset + 1]) & 0xFF) << 16) | ((ord($hash[$offset + 2]) & 0xFF) << 8) | (ord($hash[$offset + 3]) & 0xFF); return str_pad((string)($binary % 1000000), 6, '0', STR_PAD_LEFT); } function totp_verify(string $secretBase32, string $code, int $window = 1): bool { $code = trim($code); if (!preg_match('/^\d{6}$/', $code)) return false; $now = time(); for ($i = -$window; $i <= $window; $i++) { if (hash_equals(totp_code($secretBase32, $now + ($i * 30)), $code)) return true; } return false; } function totp_uri(string $issuer, string $account, string $secretBase32): string { return 'otpauth://totp/' . rawurlencode($issuer) . ':' . rawurlencode($account) . '?secret=' . $secretBase32 . '&issuer=' . rawurlencode($issuer); } function require_login(): void { if (session_status() === PHP_SESSION_NONE) { session_start(); } if (!is_logged_in()) { header('Location: /dwar?login=1'); exit; } } function password_ok(string $pass, string $stored): bool { $salted = $pass . PASSWORD_PEPPER; if (strpos($stored, '$') !== false) { return password_verify($salted, $stored); } return hash_equals($stored, hash('sha256', $salted)); } function redirect(string $url): void { header('Location: ' . $url); exit; } /** Render Open Graph + Twitter card meta tags for a page. */ function render_og_tags(array $opts): void { $title = $opts['title'] ?? ''; $desc = $opts['description'] ?? ''; $url = $opts['url'] ?? site_base_url(); $type = $opts['type'] ?? 'website'; $image = $opts['image'] ?? site_base_url() . '/img/apple-touch-icon.png'; echo ' ' . "\n"; echo ' ' . "\n"; echo ' ' . "\n"; if ($title !== '') echo ' ' . "\n"; if ($desc !== '') echo ' ' . "\n"; echo ' ' . "\n"; echo ' ' . "\n"; if ($title !== '') echo ' ' . "\n"; if ($desc !== '') echo ' ' . "\n"; echo ' ' . "\n"; } /** Render JSON-LD WebSite + Organization schema. */ function render_jsonld(): void { $base = site_base_url(); echo ' ' . "\n"; }